diff --git a/lib/baobab/generator/generate_actor_source.v b/lib/baobab/generator/generate_actor_source.v index ac34f04b..efb1e947 100644 --- a/lib/baobab/generator/generate_actor_source.v +++ b/lib/baobab/generator/generate_actor_source.v @@ -6,6 +6,19 @@ import freeflowuniverse.herolib.core.texttools import freeflowuniverse.herolib.baobab.specification { ActorInterface, ActorSpecification } import json +pub fn generate_module_from_openapi(openapi_path string) !string { + // the actor specification obtained from the OpenRPC Specification + openapi_spec := openapi.new(path: openapi_path)! + actor_spec := specification.from_openapi(openapi_spec)! + + actor_module := generator.generate_actor_module( + actor_spec, + interfaces: [.openapi, .http] + )! + + return actor_module.write_str()! +} + pub fn generate_actor_module(spec ActorSpecification, params Params) !Module { mut files := []IFile{} mut folders := []IFolder{} diff --git a/lib/core/code/model_file.v b/lib/core/code/model_file.v index c4e6b55c..64235922 100644 --- a/lib/core/code/model_file.v +++ b/lib/core/code/model_file.v @@ -5,6 +5,8 @@ import freeflowuniverse.herolib.core.pathlib import os pub interface IFile { + write(string, WriteOptions) ! + write_str(WriteOptions) !string name string write(string, WriteOptions) ! } @@ -24,6 +26,10 @@ pub fn (f File) write(path string, params WriteOptions) ! { } } +pub fn (f File) write_str(params WriteOptions) !string { + return f.content +} + pub fn (f File) typescript(path string, params WriteOptions) ! { if params.format { os.execute('npx prettier --write ${path}') @@ -100,6 +106,31 @@ pub fn (code VFile) write(path string, options WriteOptions) ! { } } +pub fn (code VFile) write_str(options WriteOptions) !string { + imports_str := code.imports.map(it.vgen()).join_lines() + + code_str := if code.content != '' { + code.content + } else { + vgen(code.items) + } + + consts_str := if code.consts.len > 1 { + stmts := code.consts.map('${it.name} = ${it.value}') + '\nconst(\n${stmts.join('\n')}\n)\n' + } else if code.consts.len == 1 { + '\nconst ${code.consts[0].name} = ${code.consts[0].value}\n' + } else { + '' + } + + mod_stmt := if code.mod == '' {''} else { + 'module ${code.mod}' + } + + return '${mod_stmt}\n${imports_str}\n${consts_str}${code_str}' +} + pub fn (file VFile) get_function(name string) ?Function { functions := file.items.filter(it is Function).map(it as Function) target_lst := functions.filter(it.name == name) diff --git a/lib/core/code/model_function.v b/lib/core/code/model_function.v index bd9bfd9d..5ebf0ec0 100644 --- a/lib/core/code/model_function.v +++ b/lib/core/code/model_function.v @@ -81,7 +81,26 @@ pub fn new_function(code string) !Function { } pub fn parse_function(code_ string) !Function { - mut code := code_.trim_space() + // Extract comments and actual function code + mut lines := code_.split_into_lines() + mut comment_lines := []string{} + mut function_lines := []string{} + mut in_function := false + + for line in lines { + trimmed := line.trim_space() + if !in_function && trimmed.starts_with('//') { + comment_lines << trimmed.trim_string_left('//').trim_space() + } else if !in_function && (trimmed.starts_with('pub fn') || trimmed.starts_with('fn')) { + in_function = true + function_lines << line + } else if in_function { + function_lines << line + } + } + + // Process the function code + mut code := function_lines.join('\n').trim_space() is_pub := code.starts_with('pub ') if is_pub { code = code.trim_string_left('pub ').trim_space() @@ -111,16 +130,33 @@ pub fn parse_function(code_ string) !Function { } else { []Param{} } + // Extract the result type, handling the ! for result types + mut result_type := code.all_after(')').all_before('{').replace(' ', '') + mut has_return := false + + // Check if the result type contains ! + if result_type.contains('!') { + has_return = true + result_type = result_type.replace('!', '') + } + result := new_param( - v: code.all_after(')').all_before('{').replace(' ', '') + v: result_type )! body := if code.contains('{') { code.all_after('{').all_before_last('}') } else { '' } + + // Process the comments into a description + description := comment_lines.join('\n') + return Function{ name: name receiver: receiver - params: params - result: result - body: body + params: params + result: result + body: body + description: description + is_pub: is_pub + has_return: has_return } } diff --git a/lib/core/code/model_function_test.v b/lib/core/code/model_function_test.v new file mode 100644 index 00000000..eae604d1 --- /dev/null +++ b/lib/core/code/model_function_test.v @@ -0,0 +1,95 @@ +module code + +fn test_parse_function_with_comments() { + // Test function string with comments + function_str := '// test_function is a simple function for testing the MCP tool code generation +// It takes a config and returns a result +pub fn test_function(config TestConfig) !TestResult { + // This is just a mock implementation for testing purposes + if config.name == \'\' { + return error(\'Name cannot be empty\') + } + + return TestResult{ + success: config.enabled + message: \'Test completed for \${config.name}\' + code: if config.enabled { 0 } else { 1 } + } +}' + + // Parse the function + function := parse_function(function_str) or { + assert false, 'Failed to parse function: ${err}' + Function{} + } + + // Verify the parsed function properties + assert function.name == 'test_function' + assert function.is_pub == true + assert function.params.len == 1 + assert function.params[0].name == 'config' + assert function.params[0].typ.symbol() == 'TestConfig' + assert function.result.typ.symbol() == 'TestResult' + + // Verify that the comments were correctly parsed into the description + expected_description := 'test_function is a simple function for testing the MCP tool code generation +It takes a config and returns a result' + assert function.description == expected_description + + println('test_parse_function_with_comments passed') +} + +fn test_parse_function_without_comments() { + // Test function string without comments + function_str := 'fn simple_function(name string, count int) string { + return \'\${name} count: \${count}\' +}' + + // Parse the function + function := parse_function(function_str) or { + assert false, 'Failed to parse function: ${err}' + Function{} + } + + // Verify the parsed function properties + assert function.name == 'simple_function' + assert function.is_pub == false + assert function.params.len == 2 + assert function.params[0].name == 'name' + assert function.params[0].typ.symbol() == 'string' + assert function.params[1].name == 'count' + assert function.params[1].typ.symbol() == 'int' + assert function.result.typ.symbol() == 'string' + + // Verify that there is no description + assert function.description == '' + + println('test_parse_function_without_comments passed') +} + +fn test_parse_function_with_receiver() { + // Test function with a receiver + function_str := 'pub fn (d &Developer) create_tool(name string) !Tool { + return Tool{ + name: name + } +}' + + // Parse the function + function := parse_function(function_str) or { + assert false, 'Failed to parse function: ${err}' + Function{} + } + + // Verify the parsed function properties + assert function.name == 'create_tool' + assert function.is_pub == true + assert function.receiver.name == 'd' + assert function.receiver.typ.symbol() == '&Developer' + assert function.params.len == 1 + assert function.params[0].name == 'name' + assert function.params[0].typ.symbol() == 'string' + assert function.result.typ.symbol() == 'Tool' + + println('test_parse_function_with_receiver passed') +} diff --git a/lib/core/code/model_module.v b/lib/core/code/model_module.v index b571974e..a920fed7 100644 --- a/lib/core/code/model_module.v +++ b/lib/core/code/model_module.v @@ -78,3 +78,13 @@ pub fn (mod Module) write(path string, options WriteOptions) ! { mut mod_file := pathlib.get_file(path: '${module_dir.path}/v.mod')! mod_file.write($tmpl('templates/v.mod.template'))! } + +pub fn (mod Module) write_str() !string { + mut out := '' + for file in mod.files { + console.print_debug("mod file write ${file.name}") + out += file.write_str()! + } + + return out +} \ No newline at end of file diff --git a/lib/core/code/model_struct.v b/lib/core/code/model_struct.v index 1fec8027..18214aa5 100644 --- a/lib/core/code/model_struct.v +++ b/lib/core/code/model_struct.v @@ -3,6 +3,7 @@ module code import log import os import freeflowuniverse.herolib.core.texttools +import strings pub struct Struct { pub mut: @@ -53,6 +54,109 @@ pub fn (struct_ Struct) vgen() string { return struct_str } +// parse_struct parses a struct definition string and returns a Struct object +// The input string should include the struct definition including any preceding comments +pub fn parse_struct(code_ string) !Struct { + // Extract comments and actual struct code + mut lines := code_.split_into_lines() + mut comment_lines := []string{} + mut struct_lines := []string{} + mut in_struct := false + mut struct_name := '' + mut is_pub := false + + for line in lines { + trimmed := line.trim_space() + if !in_struct && trimmed.starts_with('//') { + comment_lines << trimmed.trim_string_left('//').trim_space() + } else if !in_struct && (trimmed.starts_with('struct ') || trimmed.starts_with('pub struct ')) { + in_struct = true + struct_lines << line + + // Extract struct name + is_pub = trimmed.starts_with('pub ') + mut name_part := if is_pub { + trimmed.trim_string_left('pub struct ').trim_space() + } else { + trimmed.trim_string_left('struct ').trim_space() + } + + // Handle generics in struct name + if name_part.contains('<') { + struct_name = name_part.all_before('<').trim_space() + } else if name_part.contains('{') { + struct_name = name_part.all_before('{').trim_space() + } else { + struct_name = name_part + } + } else if in_struct { + struct_lines << line + + // Check if we've reached the end of the struct + if trimmed.starts_with('}') { + break + } + } + } + + if struct_name == '' { + return error('Invalid struct format: could not extract struct name') + } + + // Process the struct fields + mut fields := []StructField{} + mut current_section := '' + + for i := 1; i < struct_lines.len - 1; i++ { // Skip the first and last lines (struct declaration and closing brace) + line := struct_lines[i].trim_space() + + // Skip empty lines and comments + if line == '' || line.starts_with('//') { + continue + } + + // Check for section markers (pub:, mut:, pub mut:) + if line.ends_with(':') { + current_section = line + continue + } + + // Parse field + parts := line.split_any(' \t') + if parts.len < 2 { + continue // Skip invalid lines + } + + field_name := parts[0] + field_type_str := parts[1..].join(' ') + + // Parse the type string into a Type object + field_type := parse_type(field_type_str) + + // Determine field visibility based on section + is_pub_field := current_section.contains('pub') + is_mut_field := current_section.contains('mut') + + fields << StructField{ + name: field_name + typ: field_type + is_pub: is_pub_field + is_mut: is_mut_field + } + } + + // Process the comments into a description + description := comment_lines.join('\n') + + return Struct{ + name: struct_name + description: description + is_pub: is_pub + fields: fields + } +} + + pub struct Interface { pub mut: name string diff --git a/lib/core/code/model_struct_test.v b/lib/core/code/model_struct_test.v new file mode 100644 index 00000000..a176f7d5 --- /dev/null +++ b/lib/core/code/model_struct_test.v @@ -0,0 +1,73 @@ +module code + +fn test_parse_struct() { + // Test case 1: struct with comments and pub fields + struct_str := '// TestResult is a struct for test results +// It contains information about test execution +pub struct TestResult { +pub: + success bool + message string + code int +} +' + result := parse_struct(struct_str) or { + assert false, 'Failed to parse struct: ${err}' + Struct{} + } + + assert result.name == 'TestResult' + assert result.description == 'TestResult is a struct for test results +It contains information about test execution' + assert result.is_pub == true + assert result.fields.len == 3 + + assert result.fields[0].name == 'success' + assert result.fields[0].typ.symbol() == 'bool' + assert result.fields[0].is_pub == true + assert result.fields[0].is_mut == false + + assert result.fields[1].name == 'message' + assert result.fields[1].typ.symbol() == 'string' + assert result.fields[1].is_pub == true + assert result.fields[1].is_mut == false + + assert result.fields[2].name == 'code' + assert result.fields[2].typ.symbol() == 'int' + assert result.fields[2].is_pub == true + assert result.fields[2].is_mut == false + + // Test case 2: struct without comments and with mixed visibility + struct_str2 := 'struct SimpleStruct { +pub: + name string +mut: + count int + active bool +} +' + result2 := parse_struct(struct_str2) or { + assert false, 'Failed to parse struct: ${err}' + Struct{} + } + + assert result2.name == 'SimpleStruct' + assert result2.description == '' + assert result2.is_pub == false + assert result2.fields.len == 3 + + assert result2.fields[0].name == 'name' + assert result2.fields[0].typ.symbol() == 'string' + assert result2.fields[0].is_pub == true + assert result2.fields[0].is_mut == false + + assert result2.fields[1].name == 'count' + assert result2.fields[1].typ.symbol() == 'int' + assert result2.fields[1].is_pub == false + assert result2.fields[1].is_mut == true + + assert result2.fields[2].name == 'active' + assert result2.fields[2].typ.symbol() == 'bool' + assert result2.fields[2].is_pub == false + assert result2.fields[2].is_mut == true +} diff --git a/lib/core/code/model_types.v b/lib/core/code/model_types.v index b8d18743..3dea87d4 100644 --- a/lib/core/code/model_types.v +++ b/lib/core/code/model_types.v @@ -91,46 +91,66 @@ pub fn type_from_symbol(symbol_ string) Type { return Object{symbol} } +pub fn (t Array) symbol() string { + return '[]${t.typ.symbol()}' +} + +pub fn (t Object) symbol() string { + return t.name +} + +pub fn (t Result) symbol() string { + return '!${t.typ.symbol()}' +} + +pub fn (t Integer) symbol() string { + mut str := '' + if !t.signed { + str += 'u' + } + if t.bytes != 0 { + return '${str}${t.bytes}' + } else { + return '${str}int' + } +} + +pub fn (t Alias) symbol() string { + return t.name +} + +pub fn (t String) symbol() string { + return 'string' +} + +pub fn (t Boolean) symbol() string { + return 'bool' +} + +pub fn (t Map) symbol() string { + return 'map[string]${t.typ.symbol()}' +} + +pub fn (t Function) symbol() string { + return 'fn ()' +} + +pub fn (t Void) symbol() string { + return '' +} + pub fn (t Type) symbol() string { return match t { - Array { - '[]${t.typ.symbol()}' - } - Object { - t.name - } - Result { - '!${t.typ.symbol()}' - } - Integer { - mut str := '' - if !t.signed { - str += 'u' - } - if t.bytes != 0 { - '${str}${t.bytes}' - } else { - '${str}int' - } - } - Alias { - t.name - } - String { - 'string' - } - Boolean { - 'bool' - } - Map { - 'map[string]${t.typ.symbol()}' - } - Function { - 'fn ()' - } - Void { - '' - } + Array { t.symbol() } + Object { t.symbol() } + Result { t.symbol() } + Integer { t.symbol() } + Alias { t.symbol() } + String { t.symbol() } + Boolean { t.symbol() } + Map { t.symbol() } + Function { t.symbol() } + Void { t.symbol() } } } @@ -214,3 +234,74 @@ pub fn (t Type) empty_value() string { } } } + +// parse_type parses a type string into a Type struct +pub fn parse_type(type_str string) Type { + println('Parsing type string: "${type_str}"') + mut type_str_trimmed := type_str.trim_space() + + // Handle struct definitions by extracting just the struct name + if type_str_trimmed.contains('struct ') { + lines := type_str_trimmed.split_into_lines() + for line in lines { + if line.contains('struct ') { + mut struct_name := '' + if line.contains('pub struct ') { + struct_name = line.all_after('pub struct ').all_before('{') + } else { + struct_name = line.all_after('struct ').all_before('{') + } + struct_name = struct_name.trim_space() + println('Extracted struct name: "${struct_name}"') + return Object{struct_name} + } + } + } + + // Check for simple types first + if type_str_trimmed == 'string' { + return String{} + } else if type_str_trimmed == 'bool' || type_str_trimmed == 'boolean' { + return Boolean{} + } else if type_str_trimmed == 'int' { + return Integer{} + } else if type_str_trimmed == 'u8' { + return Integer{bytes: 8, signed: false} + } else if type_str_trimmed == 'u16' { + return Integer{bytes: 16, signed: false} + } else if type_str_trimmed == 'u32' { + return Integer{bytes: 32, signed: false} + } else if type_str_trimmed == 'u64' { + return Integer{bytes: 64, signed: false} + } else if type_str_trimmed == 'i8' { + return Integer{bytes: 8} + } else if type_str_trimmed == 'i16' { + return Integer{bytes: 16} + } else if type_str_trimmed == 'i32' { + return Integer{bytes: 32} + } else if type_str_trimmed == 'i64' { + return Integer{bytes: 64} + } + + // Check for array types + if type_str_trimmed.starts_with('[]') { + elem_type := type_str_trimmed.all_after('[]') + return Array{parse_type(elem_type)} + } + + // Check for map types + if type_str_trimmed.starts_with('map[') && type_str_trimmed.contains(']') { + value_type := type_str_trimmed.all_after(']') + return Map{parse_type(value_type)} + } + + // Check for result types + if type_str_trimmed.starts_with('!') { + result_type := type_str_trimmed.all_after('!') + return Result{parse_type(result_type)} + } + + // If no other type matches, treat as an object/struct type + println('Treating as object type: "${type_str_trimmed}"') + return Object{type_str_trimmed} +} diff --git a/lib/schemas/jsonrpc/handler.v b/lib/schemas/jsonrpc/handler.v index 16a81d89..cbcbbd4c 100644 --- a/lib/schemas/jsonrpc/handler.v +++ b/lib/schemas/jsonrpc/handler.v @@ -68,15 +68,17 @@ pub fn (handler Handler) handler(client &websocket.Client, message string) strin // - The JSON-RPC response as a string, or an error if processing fails pub fn (handler Handler) handle(message string) !string { // Extract the method name from the request + log.error('debugzo1') method := decode_request_method(message)! // log.info('Handling remote procedure call to method: ${method}') - // Look up the procedure handler for the requested method procedure_func := handler.procedures[method] or { // log.error('No procedure handler for method ${method} found') return method_not_found } + log.error('debugzo3') + // Execute the procedure handler with the request payload response := procedure_func(message) or { panic(err) } return response diff --git a/lib/vfs/vfs_local/vfs_implementation.v b/lib/vfs/vfs_local/vfs_implementation.v index 200842e2..087171a0 100644 --- a/lib/vfs/vfs_local/vfs_implementation.v +++ b/lib/vfs/vfs_local/vfs_implementation.v @@ -276,3 +276,59 @@ pub fn (mut myvfs LocalVFS) destroy() ! { } myvfs.init()! } + +// File concatenate operation - appends data to a file +pub fn (myvfs LocalVFS) file_concatenate(path string, data []u8) ! { + abs_path := myvfs.abs_path(path) + if !os.exists(abs_path) { + return error('File does not exist: ${path}') + } + if os.is_dir(abs_path) { + return error('Cannot concatenate to directory: ${path}') + } + + // Read existing content + existing_content := os.read_bytes(abs_path) or { + return error('Failed to read file ${path}: ${err}') + } + + // Create a new buffer with the combined content + mut new_content := []u8{cap: existing_content.len + data.len} + new_content << existing_content + new_content << data + + // Write back to file + os.write_file(abs_path, new_content.bytestr()) or { + return error('Failed to write concatenated data to file ${path}: ${err}') + } +} + +// Get path of an FSEntry +pub fn (myvfs LocalVFS) get_path(entry &vfs.FSEntry) !string { + // Check if the entry is a LocalFSEntry + local_entry := entry as LocalFSEntry + return local_entry.path +} + +// Print information about the VFS +pub fn (myvfs LocalVFS) print() ! { + println('LocalVFS:') + println(' Root path: ${myvfs.root_path}') + + // Print root directory contents + root_entries := myvfs.dir_list('') or { + println(' Error listing root directory: ${err}') + return + } + + println(' Root entries: ${root_entries.len}') + for entry in root_entries { + metadata := entry.get_metadata() + entry_type := match metadata.file_type { + .file { 'FILE' } + .directory { 'DIR' } + .symlink { 'LINK' } + } + println(' ${entry_type} ${metadata.name}') + } +} diff --git a/manual/serve_wiki.sh b/manual/serve_wiki.sh new file mode 100755 index 00000000..0396147f --- /dev/null +++ b/manual/serve_wiki.sh @@ -0,0 +1,297 @@ +#!/bin/bash + +# Exit on error +set -e + +echo "Starting HeroLib Manual Wiki Server..." + +# Get the directory of this script (manual directory) +MANUAL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Path to the wiki package +WIKI_DIR="/Users/timurgordon/code/github/freeflowuniverse/herolauncher/pkg/ui/wiki" + +# Path to the herolib directory +HEROLIB_DIR="/Users/timurgordon/code/github/freeflowuniverse/herolib" + +# Check if the wiki directory exists +if [ ! -d "$WIKI_DIR" ]; then + echo "Error: Wiki directory not found at $WIKI_DIR" + exit 1 +fi + +# Check if the herolib directory exists +if [ ! -d "$HEROLIB_DIR" ]; then + echo "Error: HeroLib directory not found at $HEROLIB_DIR" + exit 1 +fi + +# Create a local VFS instance for the manual directory +echo "Creating local VFS for manual directory: $MANUAL_DIR" +cd "$HEROLIB_DIR" + +# Create a temporary V program to initialize the VFS +TMP_DIR=$(mktemp -d) +VFS_INIT_FILE="$TMP_DIR/vfs_init.v" + +cat > "$VFS_INIT_FILE" << 'EOL' +module main + +import freeflowuniverse.herolib.vfs +import freeflowuniverse.herolib.vfs.vfs_local +import os + +fn main() { + if os.args.len < 2 { + println('Usage: vfs_init ') + exit(1) + } + + root_path := os.args[1] + println('Initializing local VFS with root path: ${root_path}') + + vfs_impl := vfs_local.new_local_vfs(root_path) or { + println('Error creating local VFS: ${err}') + exit(1) + } + + println('Local VFS initialized successfully') +} +EOL + +# Compile and run the VFS initialization program +cd "$TMP_DIR" +v "$VFS_INIT_FILE" +"$TMP_DIR/vfs_init" "$MANUAL_DIR" + +# Generate configuration JSON file with sidebar data +CONFIG_FILE="$TMP_DIR/wiki_config.json" +echo "Generating wiki configuration file: $CONFIG_FILE" + +# Create a temporary Go program to generate the sidebar configuration +SIDEBAR_GEN_FILE="$TMP_DIR/sidebar_gen.go" + +cat > "$SIDEBAR_GEN_FILE" << 'EOL' +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +// SidebarItem represents an item in the sidebar +type SidebarItem struct { + Title string `json:"Title"` + Href string `json:"Href"` + IsDir bool `json:"IsDir"` + External bool `json:"External,omitempty"` + Children []SidebarItem `json:"Children,omitempty"` +} + +// SidebarSection represents a section in the sidebar +type SidebarSection struct { + Title string `json:"Title"` + Items []SidebarItem `json:"Items"` +} + +// Configuration represents the wiki configuration +type Configuration struct { + Sidebar []SidebarSection `json:"Sidebar"` + Title string `json:"Title,omitempty"` + BaseURL string `json:"BaseURL,omitempty"` +} + +func main() { + if len(os.Args) < 3 { + fmt.Println("Usage: sidebar_gen ") + os.Exit(1) + } + + contentPath := os.Args[1] + outputFile := os.Args[2] + + // Generate sidebar data + sidebar, err := generateSidebarFromPath(contentPath) + if err != nil { + fmt.Printf("Error generating sidebar: %v\n", err) + os.Exit(1) + } + + // Create configuration + config := Configuration{ + Sidebar: sidebar, + Title: "HeroLib Manual", + } + + // Write to file + configJSON, err := json.MarshalIndent(config, "", " ") + if err != nil { + fmt.Printf("Error marshaling JSON: %v\n", err) + os.Exit(1) + } + + err = ioutil.WriteFile(outputFile, configJSON, 0644) + if err != nil { + fmt.Printf("Error writing file: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Configuration written to %s\n", outputFile) +} + +// Generate sidebar data from content path +func generateSidebarFromPath(contentPath string) ([]SidebarSection, error) { + // Get absolute path for content directory + absContentPath, err := filepath.Abs(contentPath) + if err != nil { + return nil, fmt.Errorf("error getting absolute path: %w", err) + } + + // Process top-level directories and files + dirs, err := ioutil.ReadDir(absContentPath) + if err != nil { + return nil, fmt.Errorf("error reading content directory: %w", err) + } + + // Create sections for each top-level directory + var sections []SidebarSection + + // Add files at the root level to a "General" section + var rootFiles []SidebarItem + + // Process directories and files + for _, dir := range dirs { + if dir.IsDir() { + // Process directory + dirPath := filepath.Join(absContentPath, dir.Name()) + // Pass the top-level directory name as the initial parent path + items, err := processDirectoryHierarchy(dirPath, absContentPath, dir.Name()) + if err != nil { + return nil, fmt.Errorf("error processing directory %s: %w", dir.Name(), err) + } + + if len(items) > 0 { + sections = append(sections, SidebarSection{ + Title: formatTitle(dir.Name()), + Items: items, + }) + } + } else if isMarkdownFile(dir.Name()) { + // Add root level markdown files to the General section + filePath := filepath.Join(absContentPath, dir.Name()) + fileItem := createSidebarItemFromFile(filePath, absContentPath, "") + rootFiles = append(rootFiles, fileItem) + } + } + + // Add root files to a General section if there are any + if len(rootFiles) > 0 { + sections = append([]SidebarSection{{ + Title: "General", + Items: rootFiles, + }}, sections...) + } + + return sections, nil +} + +// Process a directory and return a hierarchical structure of sidebar items +func processDirectoryHierarchy(dirPath, rootPath, parentPath string) ([]SidebarItem, error) { + entries, err := ioutil.ReadDir(dirPath) + if err != nil { + return nil, fmt.Errorf("error reading directory %s: %w", dirPath, err) + } + + var items []SidebarItem + + // Process all entries in the directory + for _, entry := range entries { + entryPath := filepath.Join(dirPath, entry.Name()) + relPath := filepath.Join(parentPath, entry.Name()) + + if entry.IsDir() { + // Process subdirectory + subItems, err := processDirectoryHierarchy(entryPath, rootPath, relPath) + if err != nil { + return nil, err + } + + if len(subItems) > 0 { + // Create a directory item with children + items = append(items, SidebarItem{ + Title: formatTitle(entry.Name()), + Href: "/" + relPath, // Add leading slash + IsDir: true, + Children: subItems, + }) + } + } else if isMarkdownFile(entry.Name()) { + // Process markdown file + fileItem := createSidebarItemFromFile(entryPath, rootPath, parentPath) + items = append(items, fileItem) + } + } + + return items, nil +} + +// Create a sidebar item from a file path +func createSidebarItemFromFile(filePath, rootPath, parentPath string) SidebarItem { + fileName := filepath.Base(filePath) + baseName := strings.TrimSuffix(fileName, filepath.Ext(fileName)) + relPath := filepath.Join(parentPath, baseName) + + return SidebarItem{ + Title: formatTitle(baseName), + Href: "/" + relPath, // Add leading slash for proper URL formatting + IsDir: false, + } +} + +// Format a title from a file or directory name +func formatTitle(name string) string { + // Replace underscores and hyphens with spaces + name = strings.ReplaceAll(name, "_", " ") + name = strings.ReplaceAll(name, "-", " ") + + // Capitalize the first letter of each word + words := strings.Fields(name) + for i, word := range words { + if len(word) > 0 { + words[i] = strings.ToUpper(word[0:1]) + word[1:] + } + } + + return strings.Join(words, " ") +} + +// Check if a file is a markdown file +func isMarkdownFile(fileName string) bool { + ext := strings.ToLower(filepath.Ext(fileName)) + return ext == ".md" || ext == ".markdown" +} +EOL + +# Compile and run the sidebar generator +cd "$TMP_DIR" +go build -o sidebar_gen "$SIDEBAR_GEN_FILE" +"$TMP_DIR/sidebar_gen" "$MANUAL_DIR" "$CONFIG_FILE" + +# Start the wiki server with the manual directory as the content path and config file +echo "Serving manual content from: $MANUAL_DIR" +echo "Using wiki server from: $WIKI_DIR" +cd "$WIKI_DIR" + +# Display the generated configuration for debugging +echo "Generated configuration:" +cat "$CONFIG_FILE" | head -n 30 + +# Run the wiki server on port 3004 +go run main.go "$MANUAL_DIR" "$CONFIG_FILE" 3004 + +# The script will not reach this point unless the server is stopped +echo "Wiki server stopped."