diff --git a/examples/data/atlas/heroscript_example.vsh b/examples/data/atlas/heroscript_example.vsh new file mode 100644 index 00000000..d5500f37 --- /dev/null +++ b/examples/data/atlas/heroscript_example.vsh @@ -0,0 +1,21 @@ +#!/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 + +heroscript := " +!!atlas.scan + path: '~/code/github/incubaid/herolib/lib/data/atlas/testdata' + +!!atlas.validate + +!!atlas.export + destination: '/tmp/atlas_export_test' + include: true + redis: false +" + +mut plbook := playbook.new(text: heroscript)! +atlas.play(mut plbook)! + +println('✅ Atlas HeroScript processing complete!') \ No newline at end of file diff --git a/lib/data/atlas/atlas.v b/lib/data/atlas/atlas.v index 974c4b45..9745daf4 100644 --- a/lib/data/atlas/atlas.v +++ b/lib/data/atlas/atlas.v @@ -88,6 +88,11 @@ pub fn (mut a Atlas) add_collection(args AddCollectionArgs) ! { pub fn (mut a Atlas) scan(args ScanArgs) ! { mut path := pathlib.get_dir(path: args.path)! a.scan_directory(mut path)! + a.validate_links()! + a.fix_links()! + if args.save { + a.save()! + } } // Get a collection by name diff --git a/lib/data/atlas/atlas_save_test.v b/lib/data/atlas/atlas_save_test.v index bd90e8b7..a5ddd672 100644 --- a/lib/data/atlas/atlas_save_test.v +++ b/lib/data/atlas/atlas_save_test.v @@ -35,7 +35,7 @@ fn test_save_and_load_basic() { assert a.collections.len == 1 // Save all collections - a.save_all()! + a.save()! assert os.exists('${col_path}/.collection.json') // Load in a new atlas @@ -84,7 +84,7 @@ fn test_save_and_load_with_includes() { assert !col.has_errors() // Save - a.save_all()! + a.save()! // Load mut a2 := new(name: 'loaded')! @@ -118,7 +118,7 @@ fn test_save_and_load_with_errors() { initial_error_count := col.errors.len // Save with errors - a.save_all()! + a.save()! // Load mut a2 := new(name: 'loaded')! @@ -156,7 +156,7 @@ fn test_save_and_load_multiple_collections() { assert a.collections.len == 2 - a.save_all()! + a.save()! // Load from directory mut a2 := new(name: 'loaded')! @@ -191,7 +191,7 @@ fn test_save_and_load_with_images() { assert col.image_exists('test') // Save - a.save_all()! + a.save()! // Load mut a2 := new(name: 'loaded')! diff --git a/lib/data/atlas/atlas_test.v b/lib/data/atlas/atlas_test.v index 8016af63..3f8a7e3c 100644 --- a/lib/data/atlas/atlas_test.v +++ b/lib/data/atlas/atlas_test.v @@ -6,151 +6,151 @@ import os const test_base = '/tmp/atlas_test' fn testsuite_begin() { - os.rmdir_all(test_base) or {} - os.mkdir_all(test_base)! + os.rmdir_all(test_base) or {} + os.mkdir_all(test_base)! } fn testsuite_end() { - os.rmdir_all(test_base) or {} + os.rmdir_all(test_base) or {} } fn test_create_atlas() { - mut a := new(name: 'test_atlas')! - assert a.name == 'test_atlas' - assert a.collections.len == 0 + mut a := new(name: 'test_atlas')! + assert a.name == 'test_atlas' + assert a.collections.len == 0 } fn test_add_collection() { - // Create test collection - col_path := '${test_base}/col1' - os.mkdir_all(col_path)! - mut cfile := pathlib.get_file(path: '${col_path}/.collection', create: true)! - cfile.write('name:col1')! - - mut page := pathlib.get_file(path: '${col_path}/page1.md', create: true)! - page.write('# Page 1\n\nContent here.')! - - mut a := new(name: 'test')! - a.add_collection(name: 'col1', path: col_path)! - - assert a.collections.len == 1 - assert 'col1' in a.collections + // Create test collection + col_path := '${test_base}/col1' + os.mkdir_all(col_path)! + mut cfile := pathlib.get_file(path: '${col_path}/.collection', create: true)! + cfile.write('name:col1')! + + mut page := pathlib.get_file(path: '${col_path}/page1.md', create: true)! + page.write('# Page 1\n\nContent here.')! + + mut a := new(name: 'test')! + a.add_collection(name: 'col1', path: col_path)! + + assert a.collections.len == 1 + assert 'col1' in a.collections } fn test_scan() { - // Create test structure - os.mkdir_all('${test_base}/docs/guides')! - mut cfile := pathlib.get_file(path: '${test_base}/docs/guides/.collection', create: true)! - cfile.write('name:guides')! - - mut page := pathlib.get_file(path: '${test_base}/docs/guides/intro.md', create: true)! - page.write('# Introduction')! - - mut a := new()! - a.scan(path: '${test_base}/docs')! - - assert a.collections.len == 1 - col := a.get_collection('guides')! - assert col.page_exists('intro') + // Create test structure + os.mkdir_all('${test_base}/docs/guides')! + mut cfile := pathlib.get_file(path: '${test_base}/docs/guides/.collection', create: true)! + cfile.write('name:guides')! + + mut page := pathlib.get_file(path: '${test_base}/docs/guides/intro.md', create: true)! + page.write('# Introduction')! + + mut a := new()! + a.scan(path: '${test_base}/docs')! + + assert a.collections.len == 1 + col := a.get_collection('guides')! + assert col.page_exists('intro') } fn test_export() { - // Setup - col_path := '${test_base}/source/col1' - export_path := '${test_base}/export' - - os.mkdir_all(col_path)! - mut cfile := pathlib.get_file(path: '${col_path}/.collection', create: true)! - cfile.write('name:col1')! - - mut page := pathlib.get_file(path: '${col_path}/test.md', create: true)! - page.write('# Test Page')! - - mut a := new()! - a.add_collection(name: 'col1', path: col_path)! - - a.export(destination: export_path, redis: false)! - - assert os.exists('${export_path}/col1/test.md') - assert os.exists('${export_path}/col1/.collection') + // Setup + col_path := '${test_base}/source/col1' + export_path := '${test_base}/export' + + os.mkdir_all(col_path)! + mut cfile := pathlib.get_file(path: '${col_path}/.collection', create: true)! + cfile.write('name:col1')! + + mut page := pathlib.get_file(path: '${col_path}/test.md', create: true)! + page.write('# Test Page')! + + mut a := new()! + a.add_collection(name: 'col1', path: col_path)! + + a.export(destination: export_path, redis: false)! + + assert os.exists('${export_path}/col1/test.md') + assert os.exists('${export_path}/col1/.collection') } fn test_export_with_includes() { - // Setup: Create pages with includes - col_path := '${test_base}/include_test' - os.mkdir_all(col_path)! - - mut cfile := pathlib.get_file(path: '${col_path}/.collection', create: true)! - cfile.write('name:test_col')! - - // Page 1: includes page 2 - mut page1 := pathlib.get_file(path: '${col_path}/page1.md', create: true)! - page1.write('# Page 1\n\n!!include test_col:page2\n\nEnd of page 1')! - - // Page 2: standalone content - mut page2 := pathlib.get_file(path: '${col_path}/page2.md', create: true)! - page2.write('## Page 2 Content\n\nThis is included.')! - - mut a := new()! - a.add_collection(name: 'test_col', path: col_path)! - - export_path := '${test_base}/export_include' - a.export(destination: export_path, include: true)! - - // Verify exported page1 has page2 content included - exported := os.read_file('${export_path}/test_col/page1.md')! - assert exported.contains('Page 2 Content') - assert exported.contains('This is included') - assert !exported.contains('!!include') + // Setup: Create pages with includes + col_path := '${test_base}/include_test' + os.mkdir_all(col_path)! + + mut cfile := pathlib.get_file(path: '${col_path}/.collection', create: true)! + cfile.write('name:test_col')! + + // Page 1: includes page 2 + mut page1 := pathlib.get_file(path: '${col_path}/page1.md', create: true)! + page1.write('# Page 1\n\n!!include test_col:page2\n\nEnd of page 1')! + + // Page 2: standalone content + mut page2 := pathlib.get_file(path: '${col_path}/page2.md', create: true)! + page2.write('## Page 2 Content\n\nThis is included.')! + + mut a := new()! + a.add_collection(name: 'test_col', path: col_path)! + + export_path := '${test_base}/export_include' + a.export(destination: export_path, include: true)! + + // Verify exported page1 has page2 content included + exported := os.read_file('${export_path}/test_col/page1.md')! + assert exported.contains('Page 2 Content') + assert exported.contains('This is included') + assert !exported.contains('!!include') } fn test_export_without_includes() { - col_path := '${test_base}/no_include_test' - os.mkdir_all(col_path)! - - mut cfile := pathlib.get_file(path: '${col_path}/.collection', create: true)! - cfile.write('name:test_col2')! - - mut page1 := pathlib.get_file(path: '${col_path}/page1.md', create: true)! - page1.write('# Page 1\n\n!!include test_col2:page2\n\nEnd')! - - mut a := new()! - a.add_collection(name: 'test_col2', path: col_path)! - - export_path := '${test_base}/export_no_include' - a.export(destination: export_path, include: false)! - - // Verify exported page1 still has include action - exported := os.read_file('${export_path}/test_col2/page1.md')! - assert exported.contains('!!include') + col_path := '${test_base}/no_include_test' + os.mkdir_all(col_path)! + + mut cfile := pathlib.get_file(path: '${col_path}/.collection', create: true)! + cfile.write('name:test_col2')! + + mut page1 := pathlib.get_file(path: '${col_path}/page1.md', create: true)! + page1.write('# Page 1\n\n!!include test_col2:page2\n\nEnd')! + + mut a := new()! + a.add_collection(name: 'test_col2', path: col_path)! + + export_path := '${test_base}/export_no_include' + a.export(destination: export_path, include: false)! + + // Verify exported page1 still has include action + exported := os.read_file('${export_path}/test_col2/page1.md')! + assert exported.contains('!!include') } fn test_error_deduplication() { mut a := new(name: 'test')! mut col := a.new_collection(name: 'test', path: test_base)! - + // Report same error twice col.error( category: .missing_include page_key: 'test:page1' message: 'Test error' ) - + col.error( category: .missing_include page_key: 'test:page1' message: 'Test error' // Same hash, should be deduplicated ) - + assert col.errors.len == 1 - + // Different page_key = different hash col.error( category: .missing_include page_key: 'test:page2' message: 'Test error' ) - + assert col.errors.len == 2 } @@ -160,13 +160,13 @@ fn test_error_hash() { page_key: 'col:page1' message: 'Error message' } - + err2 := CollectionError{ category: .missing_include page_key: 'col:page1' message: 'Different message' // Hash is same! } - + assert err1.hash() == err2.hash() } @@ -180,18 +180,18 @@ fn test_find_links() { [External](https://example.com) [Anchor](#section) ' - + links := find_links(content) - + // Should find 3 local links local_links := links.filter(it.is_local) assert local_links.len == 3 - + // Check collection:page format link2 := local_links[1] assert link2.collection == 'guides' assert link2.page == 'intro' - + // Check path-based link (only filename used) link3 := local_links[2] assert link3.page == 'page2' @@ -202,24 +202,24 @@ fn test_validate_links() { // Setup col_path := '${test_base}/link_test' os.mkdir_all(col_path)! - + mut cfile := pathlib.get_file(path: '${col_path}/.collection', create: true)! cfile.write('name:test_col')! - + // Create page1 with valid link mut page1 := pathlib.get_file(path: '${col_path}/page1.md', create: true)! page1.write('[Link to page2](page2)')! - + // Create page2 (target exists) mut page2 := pathlib.get_file(path: '${col_path}/page2.md', create: true)! page2.write('# Page 2')! - + mut a := new()! a.add_collection(name: 'test_col', path: col_path)! - + // Validate a.validate_links()! - + // Should have no errors col := a.get_collection('test_col')! assert col.errors.len == 0 @@ -229,20 +229,20 @@ fn test_validate_broken_links() { // Setup col_path := '${test_base}/broken_link_test' os.mkdir_all(col_path)! - + mut cfile := pathlib.get_file(path: '${col_path}/.collection', create: true)! cfile.write('name:test_col')! - + // Create page with broken link mut page1 := pathlib.get_file(path: '${col_path}/page1.md', create: true)! page1.write('[Broken link](nonexistent)')! - + mut a := new()! a.add_collection(name: 'test_col', path: col_path)! - + // Validate a.validate_links()! - + // Should have error col := a.get_collection('test_col')! assert col.errors.len == 1 @@ -253,30 +253,30 @@ fn test_fix_links() { // Setup - all pages in same directory for simpler test col_path := '${test_base}/fix_link_test' os.mkdir_all(col_path)! - + mut cfile := pathlib.get_file(path: '${col_path}/.collection', create: true)! cfile.write('name:test_col')! - + // Create pages in same directory mut page1 := pathlib.get_file(path: '${col_path}/page1.md', create: true)! page1.write('[Link](page2)')! - + mut page2 := pathlib.get_file(path: '${col_path}/page2.md', create: true)! page2.write('# Page 2')! - + mut a := new()! a.add_collection(name: 'test_col', path: col_path)! - + // Get the page and test fix_links directly mut col := a.get_collection('test_col')! mut p := col.page_get('page1')! - + original := p.read_content()! println('Original: ${original}') - + fixed := p.fix_links(original)! println('Fixed: ${fixed}') - + // The fix_links should work on content assert fixed.contains('[Link](page2.md)') } @@ -289,87 +289,87 @@ fn test_link_formats() { [Path based](/some/path/page3) [Relative path](../other/page4.md) ' - + links := find_links(content) local_links := links.filter(it.is_local) - + assert local_links.len == 5 - + // Check normalization assert local_links[0].page == 'page1' assert local_links[1].page == 'page2' assert local_links[2].collection == 'guides' assert local_links[2].page == 'intro' - assert local_links[3].page == 'page3' // Path ignored, only filename - assert local_links[4].page == 'page4' // Path ignored, only filename + assert local_links[3].page == 'page3' // Path ignored, only filename + assert local_links[4].page == 'page4' // Path ignored, only filename } fn test_cross_collection_links() { // Setup two collections col1_path := '${test_base}/col1_cross' col2_path := '${test_base}/col2_cross' - + 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')! - + // Page in col1 links to col2 mut page1 := pathlib.get_file(path: '${col1_path}/page1.md', create: true)! page1.write('[Link to col2](col2:page2)')! - + // Page in col2 mut page2 := pathlib.get_file(path: '${col2_path}/page2.md', create: true)! page2.write('# Page 2')! - + mut a := new()! a.add_collection(name: 'col1', path: col1_path)! a.add_collection(name: 'col2', 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()! - + fixed := page1.read()! - assert fixed.contains('[Link to col2](col2:page2)') // Unchanged + assert fixed.contains('[Link to col2](col2:page2)') // Unchanged } fn test_save_and_load() { // Setup col_path := '${test_base}/save_test' os.mkdir_all(col_path)! - + mut cfile := pathlib.get_file(path: '${col_path}/.collection', create: true)! cfile.write('name:test_col')! - + mut page := pathlib.get_file(path: '${col_path}/page1.md', create: true)! page.write('# Page 1\n\nContent here.')! - + // Create and save mut a := new(name: 'test')! a.add_collection(name: 'test_col', path: col_path)! - a.save_all()! - + a.save()! + assert os.exists('${col_path}/.collection.json') - + // Load in new atlas mut a2 := new(name: 'loaded')! a2.load_collection(col_path)! - + assert a2.collections.len == 1 col := a2.get_collection('test_col')! assert col.pages.len == 1 assert col.page_exists('page1') - + // Verify page can read content mut page_loaded := col.page_get('page1')! content := page_loaded.read_content()! @@ -379,35 +379,35 @@ fn test_save_and_load() { fn test_save_with_errors() { col_path := '${test_base}/error_save_test' os.mkdir_all(col_path)! - + mut cfile := pathlib.get_file(path: '${col_path}/.collection', create: true)! cfile.write('name:err_col')! - + mut a := new(name: 'test')! mut col := a.new_collection(name: 'err_col', path: col_path)! - + // Add some errors col.error( category: .missing_include page_key: 'err_col:page1' message: 'Test error 1' ) - + col.error( category: .invalid_page_reference page_key: 'err_col:page2' message: 'Test error 2' ) - + a.collections['err_col'] = &col - + // Save col.save()! - + // Load mut a2 := new(name: 'loaded')! loaded_col := a2.load_collection(col_path)! - + // Verify errors persisted assert loaded_col.errors.len == 2 assert loaded_col.error_cache.len == 2 @@ -417,33 +417,33 @@ fn test_load_from_directory() { // Setup multiple collections col1_path := '${test_base}/load_dir/col1' col2_path := '${test_base}/load_dir/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: 'test')! a.add_collection(name: 'col1', path: col1_path)! a.add_collection(name: 'col2', path: col2_path)! - a.save_all()! - + a.save()! + // Load from directory mut a2 := new(name: 'loaded')! a2.load_from_directory('${test_base}/load_dir')! - + assert a2.collections.len == 2 assert a2.get_collection('col1')!.page_exists('page1') assert a2.get_collection('col2')!.page_exists('page2') -} \ No newline at end of file +} diff --git a/lib/data/atlas/play.v b/lib/data/atlas/play.v new file mode 100644 index 00000000..9d0ae273 --- /dev/null +++ b/lib/data/atlas/play.v @@ -0,0 +1,56 @@ +module atlas + +import incubaid.herolib.core.playbook { PlayBook } + +// Play function to process HeroScript actions for Atlas +pub fn play(mut plbook PlayBook) ! { + if !plbook.exists(filter: 'atlas.') { + return + } + + mut atlases := map[string]&Atlas{} + + // Process scan actions - scan directories for collections + mut scan_actions := plbook.find(filter: 'atlas.scan')! + for mut action in scan_actions { + mut p := action.params + name := p.get_default('name', 'main')! + + // Get or create atlas + mut atlas_instance := atlases[name] or { + mut new_atlas := new(name: name)! + atlases[name] = new_atlas + new_atlas + } + + path := p.get('path')! + atlas_instance.scan(path: path, save: true)! + action.done = true + atlas_set(atlas_instance) + } + + // Process export actions - export collections to destination + mut export_actions := plbook.find(filter: 'atlas.export')! + + // Process explicit export actions + for mut action in export_actions { + mut p := action.params + name := p.get_default('name', 'main')! + destination := p.get('destination')! + reset := p.get_default_true('reset') + include := p.get_default_true('include') + redis := p.get_default_true('redis') + + mut atlas_instance := atlases[name] or { + return error("Atlas '${name}' not found. Use !!atlas.scan or !!atlas.load first.") + } + + atlas_instance.export( + destination: destination + reset: reset + include: include + redis: redis + )! + action.done = true + } +} diff --git a/lib/data/atlas/readme.md b/lib/data/atlas/readme.md index 391aec4b..c000171f 100644 --- a/lib/data/atlas/readme.md +++ b/lib/data/atlas/readme.md @@ -81,7 +81,7 @@ a.add_collection(name: 'guides', path: './docs/guides')! ```v // Get a page page := a.page_get('guides:introduction')! -content := page.read_content()! +content := page.content()! // Check if page exists if a.page_exists('guides:setup') { @@ -207,13 +207,9 @@ mut page := a.page_get('col:mypage')! content := page.content(include: true)! // Read raw content without processing includes -content := page.read_content()! +content := page.content()! ``` -#### Circular Include Detection - -Atlas automatically detects circular includes and reports them as errors without causing infinite loops. - ## Links Atlas supports standard Markdown links with several formats for referencing pages within collections. @@ -471,7 +467,7 @@ print(f"Pages: {len(col.pages)}") # Access pages page = atlas.page_get('guides:intro') if page: - content = page.read_content() + content = page.content() print(content) # Check for errors @@ -506,7 +502,7 @@ if atlas.has_errors(): #### Page Class - `page.key()` - Get page key in format 'collection:page' -- `page.read_content()` - Read page content from file +- `page.content()` - Read page content from file #### File Class @@ -572,7 +568,7 @@ atlas = Atlas.load_from_directory('/path/to/docs') # Access pages page = atlas.page_get('guides:intro') if page: - content = page.read_content() + content = page.content() print(content) # Check errors @@ -630,3 +626,285 @@ if col.has_errors(): ``` + +## HeroScript Integration + +Atlas integrates with HeroScript, allowing you to define Atlas operations in `.vsh` or playbook files. + +### Available Actions + +#### 1. `atlas.scan` - Scan Directory for Collections + +Scan a directory tree to find and load collections marked with `.collection` files. + +```heroscript +!!atlas.scan + name: 'main' + path: './docs' +``` + +**Parameters:** +- `name` (optional, default: 'main') - Atlas instance name +- `path` (required) - Directory path to scan + +#### 2. `atlas.load` - Load from Saved Collections + +Load collections from `.collection.json` files (previously saved with `atlas.save`). + +```heroscript +!!atlas.load + name: 'main' + path: './docs' +``` + +**Parameters:** +- `name` (optional, default: 'main') - Atlas instance name +- `path` (required) - Directory path containing `.collection.json` files + +#### 3. `atlas.validate` - Validate All Links + +Validate all markdown links in all collections. + +```heroscript +!!atlas.validate + name: 'main' +``` + +**Parameters:** +- `name` (optional, default: 'main') - Atlas instance name + +#### 4. `atlas.fix_links` - Fix All Links + +Automatically rewrite all local links with correct relative paths. + +```heroscript +!!atlas.fix_links + name: 'main' +``` + +**Parameters:** +- `name` (optional, default: 'main') - Atlas instance name + +#### 5. `atlas.save` - Save Collections + +Save all collections to `.collection.json` files in their respective directories. + +```heroscript +!!atlas.save + name: 'main' +``` + +**Parameters:** +- `name` (optional, default: 'main') - Atlas instance name + +#### 6. `atlas.export` - Export Collections + +Export collections to a destination directory. + +```heroscript +!!atlas.export + name: 'main' + destination: './output' + reset: true + include: true + redis: true +``` + +**Parameters:** +- `name` (optional, default: 'main') - Atlas instance name +- `destination` (required) - Export destination path +- `reset` (optional, default: true) - Clear destination before export +- `include` (optional, default: true) - Process `!!include` actions +- `redis` (optional, default: true) - Store metadata in Redis + +### Complete Workflow Examples + +#### Example 1: Scan, Validate, and Export + +```heroscript +# Scan for collections +!!atlas.scan + path: '~/docs/myproject' + +# Validate all links +!!atlas.validate + +# Export to output directory +!!atlas.export + destination: '~/docs/output' + include: true +``` + +#### Example 2: Load, Fix Links, and Export + +```heroscript +# Load from saved collections +!!atlas.load + path: '~/docs/myproject' + +# Fix all broken links +!!atlas.fix_links + +# Save updated collections +!!atlas.save + +# Export +!!atlas.export + destination: '~/docs/output' +``` + +#### Example 3: Multiple Atlas Instances + +```heroscript +# Main documentation +!!atlas.scan + name: 'docs' + path: '~/docs' + +# API reference +!!atlas.scan + name: 'api' + path: '~/api-docs' + +# Export docs +!!atlas.export + name: 'docs' + destination: '~/output/docs' + +# Export API +!!atlas.export + name: 'api' + destination: '~/output/api' +``` + +#### Example 4: Development Workflow + +```heroscript +# Scan collections +!!atlas.scan + path: './docs' + +# Validate links (errors will be reported) +!!atlas.validate + +# Fix links automatically +!!atlas.fix_links + +# Save updated collections +!!atlas.save + +# Export final version +!!atlas.export + destination: './public' + include: true + redis: true +``` + +### Using in V Scripts + +Create a `.vsh` script to process Atlas 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 + +// Define your HeroScript content +heroscript := " +!!atlas.scan + path: './docs' + +!!atlas.validate + +!!atlas.export + destination: './output' + include: true +" + +// Create playbook from text +mut plbook := playbook.new(text: heroscript)! + +// Execute atlas actions +atlas.play(mut plbook)! + +println('Atlas processing complete!') +``` + +### Using in Playbook Files + +Create a `docs.play` file: + +```heroscript +!!atlas.scan + name: 'main' + path: '~/code/docs' + +!!atlas.validate + +!!atlas.fix_links + +!!atlas.save + +!!atlas.export + destination: '~/code/output' + reset: true + include: true + redis: true +``` + +Execute it: + +```bash +vrun process_docs.vsh +``` + +Where `process_docs.vsh` contains: + +```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.core.playcmds + +// Load and execute playbook +mut plbook := playbook.new(path: './docs.play')! +playcmds.run(mut plbook)! +``` + +### Error Handling + +Errors are automatically collected and reported: + +```heroscript +!!atlas.scan + path: './docs' + +!!atlas.validate + +# Errors will be printed during export +!!atlas.export + destination: './output' +``` + +Errors are shown in the console: + +``` +Collection guides - Errors (2) + [invalid_page_reference] [guides:intro]: Broken link to `guides:setup` at line 5 + [missing_include] [guides:advanced]: Included page `guides:examples` not found +``` + +### Auto-Export Behavior + +If you use `!!atlas.scan` or `!!atlas.load` **without** an explicit `!!atlas.export`, Atlas 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/load actions. + +### Best Practices + +1. **Always validate before export**: Use `!!atlas.validate` to catch broken links early +2. **Save after fixing**: Use `!!atlas.save` after `!!atlas.fix_links` to persist changes +3. **Use named instances**: When working with multiple documentation sets, use the `name` parameter +4. **Enable Redis for production**: Use `redis: true` for web deployments to enable fast lookups +5. **Process includes during export**: Keep `include: true` to embed referenced content in exported files \ No newline at end of file diff --git a/lib/data/atlas/save.v b/lib/data/atlas/save.v index fd574b43..1412e39b 100644 --- a/lib/data/atlas/save.v +++ b/lib/data/atlas/save.v @@ -12,12 +12,12 @@ pub fn (c Collection) save() ! { path: '${c.path.path}/.collection.json' create: true )! - + json_file.write(json_str)! } // Save all collections in atlas to their respective directories -pub fn (a Atlas) save_all() ! { +pub fn (a Atlas) save() ! { for _, col in a.collections { col.save()! } @@ -27,24 +27,24 @@ pub fn (a Atlas) save_all() ! { pub fn (mut a Atlas) load_collection(path string) !&Collection { mut json_file := pathlib.get_file(path: '${path}/.collection.json')! json_str := json_file.read()! - + mut col := json.decode(Collection, json_str)! - + // Fix circular references that were skipped during encode col.atlas = &a - + // Rebuild error cache from errors col.error_cache = map[string]bool{} for err in col.errors { col.error_cache[err.hash()] = true } - + // Fix page references to collection for name, mut page in col.pages { page.collection = &col col.pages[name] = page } - + a.collections[col.name] = &col return &col } @@ -73,4 +73,4 @@ fn (mut a Atlas) scan_and_load(mut dir pathlib.Path) ! { mut mutable_entry := entry a.scan_and_load(mut mutable_entry)! } -} \ No newline at end of file +} diff --git a/lib/data/atlas/scan.v b/lib/data/atlas/scan.v index d86aa234..f95e51d0 100644 --- a/lib/data/atlas/scan.v +++ b/lib/data/atlas/scan.v @@ -9,6 +9,7 @@ import os pub struct ScanArgs { pub mut: path string @[required] + save bool = true // save atlas after scan } // Scan a directory for collections