data.atlas #
Atlas Module
A lightweight document collection manager for V, inspired by doctree but simplified.
Features
- Simple Collection Scanning: Automatically find collections marked with
.collectionfiles - Include Processing: Process
!!includeactions to embed content from other pages - Easy Export: Copy files to destination with organized structure
- Optional Redis: Store metadata in Redis for quick lookups and caching
- Type-Safe Access: Get pages, images, and files with error handling
- Error Tracking: Built-in error collection and reporting with deduplication
Quick Start
put in .hero file and execute with hero or but shebang line on top of .hero script
Scan Parameters:
name(optional, default: 'main') - Atlas instance namepath(required when git_url not provided) - Directory path to scangit_url(alternative to path) - Git repository URL to clone/checkoutgit_root(optional when using git_url, default: ~/code) - Base directory for cloningmeta_path(optional) - Directory to save collection metadata JSONignore(optional) - List of directory names to skip during scan
most basic example
#!/usr/bin/env hero
!!atlas.scan git_url:"https://git.ourworld.tf/tfgrid/docs_tfgrid4/src/branch/main/collections/tests"
!!atlas.export destination: '/tmp/atlas_export'
put this in .hero file
usage in herolib
import incubaid.herolib.data.atlas
// Create a new Atlas
mut a := atlas.new(name: 'my_docs')!
// Scan a directory for collections
a.scan(path: '/path/to/docs')!
// Export to destination
a.export(destination: '/path/to/output')!
Collections
Collections are directories marked with a .collection file.
.collection File Format
name:my_collection
Core Concepts
Collections
A collection is a directory containing:- A .collection file (marks the directory as a collection)
- Markdown pages (
.mdfiles) - Images (
.png,.jpg,.jpeg,.gif,.svg) - Other files
Page Keys
Pages, images, and files are referenced using the format: collection:name
// Get a page
page := a.page_get('guides:introduction')!
// Get an image
img := a.image_get('guides:logo')!
// Get a file
file := a.file_get('guides:diagram')!
Usage Examples
Scanning for Collections
mut a := atlas.new()!
a.scan(path: './docs')!
Adding a Specific Collection
a.add_collection(name: 'guides', path: './docs/guides')!
Getting Pages
// Get a page
page := a.page_get('guides:introduction')!
content := page.content()!
// Check if page exists
if a.page_exists('guides:setup') {
println('Setup guide found')
}
Getting Images and Files
// Get an image
img := a.image_get('guides:logo')!
println('Image path: ${img.path.path}')
println('Image type: ${img.ftype}') // .image
// Get a file
file := a.file_get('guides:diagram')!
println('File name: ${file.file_name()}')
// Check existence
if a.image_exists('guides:screenshot') {
println('Screenshot found')
}
Listing All Pages
pages_map := a.list_pages()
for col_name, page_names in pages_map {
println('Collection: ${col_name}')
for page_name in page_names {
println(' - ${page_name}')
}
}
Exporting
// Full export with all features
a.export(
destination: './output'
reset: true // Clear destination before export
include: true // Process !!include actions
redis: true // Store metadata in Redis
)!
// Export without Redis
a.export(
destination: './output'
redis: false
)!
Error Handling
// Export and check for errors
a.export(destination: './output')!
// Errors are automatically printed during export
// You can also access them programmatically
for _, col in a.collections {
if col.has_errors() {
errors := col.get_errors()
for err in errors {
println('Error: ${err.str()}')
}
// Get error summary by category
summary := col.error_summary()
for category, count in summary {
println('${category}: ${count} errors')
}
}
}
Include Processing
Atlas supports simple include processing using !!include actions:
// Export with includes processed (default)
a.export(
destination: './output'
include: true // default
)!
// Export without processing includes
a.export(
destination: './output'
include: false
)!
Include Syntax
In your markdown files:
##
!!include collection:page_name
More content here
Or within the same collection:
!!include page_name
The !!include action will be replaced with the content of the referenced page during export.
Reading Pages with Includes
// Read with includes processed (default)
mut page := a.page_get('col:mypage')!
content := page.content(include: true)!
// Read raw content without processing includes
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.
Automatic Detection
When scanning collections, Atlas walks up the directory tree to find the .git directory and captures:- git_url: The remote origin URL
- git_branch: The current branch
Scanning from Git URL
You can scan collections directly from a git repository:
!!atlas.scan
name: 'my_docs'
git_url: 'https://github.com/myorg/docs.git'
git_root: '~/code' // optional, defaults to ~/code
The repository will be automatically cloned if it doesn't exist locally.
Accessing Edit URLs
mut page := atlas.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
Export with Source Information
When exporting, the git URL is displayed:
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.
Link Formats
1. Explicit Collection Reference
Link to a page in a specific collection:
[Click here](guides:introduction)
[Click here](guides:introduction.md)
2. Same Collection Reference
Link to a page in the same collection (collection name omitted):
[Click here](introduction)
3. Path-Based Reference
Link using a path - only the filename is used for matching:
[Click here](some/path/introduction)
[Click here](/absolute/path/introduction)
[Click here](path/to/introduction.md)
Important: Paths are ignored during link resolution. Only the page name (filename) is used to find the target page within the same collection.
Link Processing
Validation
Check all links in your Atlas:
mut a := atlas.new()!
a.scan(path: './docs')!
// Validate all links
a.validate_links()!
// Check for errors
for _, col in a.collections {
if col.has_errors() {
col.print_errors()
}
}
Fixing Links
Automatically rewrite links with correct relative paths:
mut a := atlas.new()!
a.scan(path: './docs')!
// Fix all links in place
a.fix_links()!
// Or fix links in a specific collection
mut col := a.get_collection('guides')!
col.fix_links()!
What fix_links() does:- Finds all local page links
- Calculates correct relative paths
- Rewrites links as
[text](relative/path/pagename.md) - Only fixes links within the same collection
- Preserves
!!includeactions unchanged - Writes changes back to files
Example
Before fix:
##
[Introduction](introduction)
[Setup](/some/old/path/setup)
[Guide](guides:advanced)
After fix (assuming pages are in subdirectories):
##
[Introduction](../intro/introduction.md)
[Setup](setup.md)
[Guide](guides:advanced) <!-- Cross-collection link unchanged -->
Link Rules
- Name Normalization: All page names are normalized using
name_fix()(lowercase, underscores, etc.) - Same Collection Only:
fix_links()only rewrites links within the same collection - Cross-Collection Links: Links with explicit collection references (e.g.,
guides:page) are validated but not rewritten - External Links: HTTP(S), mailto, and anchor links are ignored
- Error Reporting: Broken links are reported with file, line number, and link details
Export Directory Structure
When you export an Atlas, the directory structure is organized as:
$$\text{export_dir}/ \begin{cases} \text{content/} \ \quad \text{collection_name/} \ \quad \quad \text{page1.md} \ \quad \quad \text{page2.md} \ \quad \quad \text{img/} & \text{(images)} \ \quad \quad \quad \text{logo.png} \ \quad \quad \quad \text{banner.jpg} \ \quad \quad \text{files/} & \text{(other files)} \ \quad \quad \quad \text{data.csv} \ \quad \quad \quad \text{document.pdf} \ \text{meta/} & \text{(metadata)} \ \quad \text{collection_name.json} \end{cases}$$
- Pages: Markdown files directly in collection directory
- Images: Stored in
img/subdirectory - Files: Other resources stored in
files/subdirectory - Metadata: JSON files in
meta/directory with collection information
Redis Integration
Atlas 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:
- Collection Paths - Hash:
atlas:path
- Key: collection name
- Value: exported collection directory path
- Collection Contents - Hash:
atlas:<collection_name>
- Pages:
page_name→page_name.md - Images:
image_name.ext→img/image_name.ext - Files:
file_name.ext→files/file_name.ext
Redis Usage Examples
import incubaid.herolib.data.atlas
import incubaid.herolib.core.base
// Export with Redis metadata (default)
mut a := atlas.new(name: 'docs')!
a.scan(path: './docs')!
a.export(
destination: './output'
redis: true // Store metadata in Redis
)!
// Later, retrieve metadata from Redis
mut context := base.context()!
mut redis := context.redis()!
// Get collection path
col_path := redis.hget('atlas:path', 'guides')!
println('Guides collection exported to: ${col_path}')
// Get page location
page_path := redis.hget('atlas:guides', 'introduction')!
println('Introduction page: ${page_path}') // Output: introduction.md
// Get image location
img_path := redis.hget('atlas:guides', 'logo.png')!
println('Logo image: ${img_path}') // Output: img/logo.png
Saving Collections (Beta)
Status: Basic save functionality is implemented. Load functionality is work-in-progress.
Saving to JSON
Save collection metadata to JSON files for archival or cross-tool compatibility:
import incubaid.herolib.data.atlas
mut a := atlas.new(name: 'my_docs')!
a.scan(path: './docs')!
// Save all collections to a specified directory
// Creates: ${save_path}/${collection_name}.json
a.save('./metadata')!
What Gets Saved
Each .json file contains:- Collection metadata (name, path, git URL, git branch)
- All pages (with paths and collection references)
- All images and files (with paths and types)
- All errors (category, page_key, message, file)
Storage Location
save_path/
├── collection1.json
├── collection2.json
└── collection3.json
HeroScript Integration
Atlas integrates with HeroScript, allowing you to define Atlas operations in .vsh or playbook files.
Using in V Scripts
Create a .vsh script to process Atlas operations:
#!/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.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:
!!atlas.scan
name: 'main'
path: '~/code/docs'
!!atlas.export
destination: '~/code/output'
reset: true
include: true
redis: true
Execute it:
vrun process_docs.vsh
Where process_docs.vsh contains:
#!/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:
!!atlas.scan
path: './docs'
##!!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 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 actions.
Best Practices
- Always validate before export: Use
!!atlas.validateto catch broken links early - Use named instances: When working with multiple documentation sets, use the
nameparameter - Enable Redis for production: Use
redis: truefor web deployments to enable fast lookups - Process includes during export: Keep
include: trueto embed referenced content in exported files
Roadmap - Not Yet Implemented
The following features are planned but not yet available:
- Load collections from
.collection.jsonfiles - Python API for reading collections
atlas.validateplaybook actionatlas.fix_linksplaybook action- Auto-save on collection modifications
- Collection version control
fn exists #
fn exists(name string) bool
Check if Atlas exists
fn get #
fn get(name string) !&Atlas
Get Atlas from global map
fn list #
fn list() []string
List all Atlas names
fn new #
fn new(args AtlasNewArgs) !&Atlas
Create a new Atlas
fn new_group #
fn new_group(args GroupNewArgs) !Group
Create a new Group
fn play #
fn play(mut plbook PlayBook) !
Play function to process HeroScript actions for Atlas
enum CollectionErrorCategory #
enum CollectionErrorCategory {
circular_include
missing_include
include_syntax_error
invalid_page_reference
invalid_file_reference
file_not_found
invalid_collection
general_error
acl_denied // NEW: Access denied by ACL
}
enum FileType #
enum FileType {
file
image
}
enum LinkFileType #
enum LinkFileType {
page // Default: link to another page
file // Link to a non-image file
image // Link to an image file
}
enum LinkStatus #
enum LinkStatus {
init
external
found
not_found
anchor
error
}
struct Atlas #
struct Atlas {
pub mut:
name string
collections map[string]&Collection
groups map[string]&Group // name -> Group mapping
}
fn (Atlas) export #
fn (mut a Atlas) export(args ExportArgs) !
Export all collections
fn (Atlas) file_exists #
fn (a Atlas) file_exists(key string) !bool
Check if file exists
fn (Atlas) file_get #
fn (a Atlas) file_get(key string) !&File
Get a file from any collection using format "collection:file"
fn (Atlas) file_or_image_exists #
fn (a Atlas) file_or_image_exists(key string) !bool
fn (Atlas) file_or_image_get #
fn (a Atlas) file_or_image_get(key string) !&File
Get a file (can be image) from any collection using format "collection:file"
fn (Atlas) fix_links #
fn (mut a Atlas) fix_links() !
Fix all links in all collections (rewrite source files)
fn (Atlas) get_collection #
fn (a Atlas) get_collection(name string) !&Collection
Get a collection by name
fn (Atlas) group_add #
fn (mut a Atlas) group_add(mut group Group) !
Add a group to the atlas
fn (Atlas) group_get #
fn (a Atlas) group_get(name string) !&Group
Get a group by name
fn (Atlas) groups_get #
fn (a Atlas) groups_get(session Session) []&Group
Get all groups matching a session's email
fn (Atlas) image_exists #
fn (a Atlas) image_exists(key string) !bool
Check if image exists
fn (Atlas) image_get #
fn (a Atlas) image_get(key string) !&File
Get an image from any collection using format "collection:image"
fn (Atlas) init_post #
fn (mut a Atlas) init_post() !
Validate all links in all collections
fn (Atlas) list_pages #
fn (a Atlas) list_pages() map[string][]string
List all pages in Atlas
fn (Atlas) page_exists #
fn (a Atlas) page_exists(key string) !bool
Check if page exists
fn (Atlas) page_get #
fn (a Atlas) page_get(key string) !&Page
Get a page from any collection using format "collection:page"
fn (Atlas) scan #
fn (mut a Atlas) scan(args ScanArgs) !
fn (Atlas) validate_links #
fn (mut a Atlas) validate_links() !
Validate all links in all collections
struct AtlasNewArgs #
struct AtlasNewArgs {
pub mut:
name string = 'default'
}
struct Collection #
struct Collection {
pub mut:
name string
path string // absolute path
pages map[string]&Page
files map[string]&File
atlas &Atlas @[skip; str: skip]
errors []CollectionError
error_cache map[string]bool
git_url string
acl_read []string // Group names allowed to read (lowercase)
acl_write []string // Group names allowed to write (lowercase)
}
fn (Collection) can_read #
fn (c Collection) can_read(session Session) bool
Check if session can read this collection
fn (Collection) can_write #
fn (c Collection) can_write(session Session) bool
Check if session can write this collection
fn (Collection) clear_errors #
fn (mut c Collection) clear_errors()
Clear all errors
fn (Collection) error #
fn (mut c Collection) error(args CollectionErrorArgs)
Report an error, avoiding duplicates based on hash
fn (Collection) error_summary #
fn (c Collection) error_summary() map[CollectionErrorCategory]int
Get error summary by category
fn (Collection) export #
fn (mut c Collection) export(args CollectionExportArgs) !
Export a single collection Export a single collection with recursive link processing
fn (Collection) file_exists #
fn (c Collection) file_exists(name_ string) !bool
Check if file exists
fn (Collection) file_get #
fn (c Collection) file_get(name_ string) !&File
Get a file by name
fn (Collection) file_or_image_exists #
fn (c Collection) file_or_image_exists(name_ string) !bool
fn (Collection) file_or_image_get #
fn (c Collection) file_or_image_get(name_ string) !&File
fn (Collection) fix_links #
fn (mut c Collection) fix_links() !
Fix all links in collection (rewrite files)
fn (Collection) get_errors #
fn (c Collection) get_errors() []CollectionError
Get all errors
fn (Collection) has_errors #
fn (c Collection) has_errors() bool
Check if collection has errors
fn (Collection) image_exists #
fn (c Collection) image_exists(name_ string) !bool
Check if image exists
fn (Collection) image_get #
fn (c Collection) image_get(name_ string) !&File
Get an image by name
fn (Collection) page_exists #
fn (c Collection) page_exists(name_ string) !bool
Check if page exists
fn (Collection) page_get #
fn (c Collection) page_get(name_ string) !&Page
Get a page by name
fn (Collection) path #
fn (mut c Collection) path() !pathlib.Path
Read content without processing includes
fn (Collection) print_errors #
fn (c Collection) print_errors()
Print all errors to console
fn (Collection) scan_groups #
fn (mut c Collection) scan_groups() !
scan_groups scans the collection's directory for .group files and loads them into memory.
fn (Collection) validate_links #
fn (mut c Collection) validate_links() !
Validate all links in collection
struct CollectionError #
struct CollectionError {
pub mut:
category CollectionErrorCategory
page_key string // Format: "collection:page" or just collection name
message string
file string // Optional: specific file path if relevant
}
fn (CollectionError) hash #
fn (e CollectionError) hash() string
Generate MD5 hash for error deduplication Hash is based on category + page_key (or file if page_key is empty)
fn (CollectionError) str #
fn (e CollectionError) str() string
Get human-readable error message
fn (CollectionError) category_str #
fn (e CollectionError) category_str() string
Get category as string
struct CollectionErrorArgs #
struct CollectionErrorArgs {
pub mut:
category CollectionErrorCategory @[required]
message string @[required]
page_key string
file string
show_console bool // Show error in console immediately
log_error bool = true // Log to errors array (default: true)
}
struct CollectionExportArgs #
struct CollectionExportArgs {
pub mut:
destination pathlib.Path @[required]
reset bool = true
include bool = true // process includes during export
redis bool = true
}
struct CollectionNotFound #
struct CollectionNotFound {
Error
pub:
name string
msg string
}
fn (CollectionNotFound) msg #
fn (err CollectionNotFound) msg() string
struct ExportArgs #
struct ExportArgs {
pub mut:
destination string @[requireds]
reset bool = true
include bool = true
redis bool = true
}
struct File #
struct File {
pub mut:
name string // name with extension
path string // relative path of file in the collection
ftype FileType // file or image
collection &Collection @[skip; str: skip] // Reference to parent collection
}
fn (File) path #
fn (mut f File) path() !pathlib.Path
Read content without processing includes
fn (File) is_image #
fn (f File) is_image() bool
fn (File) ext #
fn (f File) ext() string
struct FileNotFound #
struct FileNotFound {
Error
pub:
collection string
file string
}
fn (FileNotFound) msg #
fn (err FileNotFound) msg() string
struct FixLinksArgs #
struct FixLinksArgs {
include bool // Process includes before fixing links
cross_collection bool // Process cross-collection links (for export)
export_mode bool // Use export-style simple paths instead of filesystem paths
}
//////////////FIX PAGES FOR THE LINKS///////////////////////
struct Group #
struct Group {
pub mut:
name string // normalized to lowercase
patterns []string // email patterns, normalized to lowercase
}
fn (Group) matches #
fn (g Group) matches(email string) bool
Check if email matches any pattern in this group
struct GroupNewArgs #
struct GroupNewArgs {
pub mut:
name string @[required]
patterns []string @[required]
}
struct Link #
struct Link {
pub mut:
src string // Source content where link was found (what to replace)
text string // Link text [text]
target string // Original link target (the source text)
line int // Line number where link was found (1-based)
pos int // Character position in line where link starts (0-based)
target_collection_name string
target_item_name string
status LinkStatus
file_type LinkFileType // Type of the link target: file, image, or page (default)
page &Page @[skip; str: skip] // Reference to page where this link is found
}
Link represents a markdown link found in content
fn (Link) target_page #
fn (mut self Link) target_page() !&Page
Get the target page this link points to
fn (Link) target_file #
fn (mut self Link) target_file() !&File
Get the target file this link points to
struct NewPageArgs #
struct NewPageArgs {
pub:
name string @[required]
path string @[required]
collection_name string @[required]
collection &Collection @[required]
}
struct Page #
struct Page {
pub mut:
name string
path string // in collection
collection_name string
links []Link
// macros []Macro
collection &Collection @[skip; str: skip] // Reference to parent collection
}
fn (Page) path #
fn (mut p Page) path() !pathlib.Path
Read content without processing includes
fn (Page) content #
fn (mut p Page) content(args ReadContentArgs) !string
Read content without processing includes
fn (Page) key #
fn (p Page) key() string
struct PageNotFound #
struct PageNotFound {
Error
pub:
collection string
page string
}
fn (PageNotFound) msg #
fn (err PageNotFound) msg() string
struct ReadContentArgs #
struct ReadContentArgs {
pub mut:
include bool
}
Read content with includes processed (default behavior)
struct ScanArgs #
struct ScanArgs {
pub mut:
path string @[required]
ignore []string // list of directory names to ignore
}
Scan a path for collections
struct Session #
struct Session {
pub mut:
user string // username
email string // user's email (lowercase internally)
params Params // additional context from request/webserver
}
- README
- fn exists
- fn get
- fn list
- fn new
- fn new_group
- fn play
- enum CollectionErrorCategory
- enum FileType
- enum LinkFileType
- enum LinkStatus
- struct Atlas
- struct AtlasNewArgs
- struct Collection
- fn can_read
- fn can_write
- fn clear_errors
- fn error
- fn error_summary
- fn export
- fn file_exists
- fn file_get
- fn file_or_image_exists
- fn file_or_image_get
- fn fix_links
- fn get_errors
- fn has_errors
- fn image_exists
- fn image_get
- fn page_exists
- fn page_get
- fn path
- fn print_errors
- fn scan_groups
- fn validate_links
- struct CollectionError
- struct CollectionErrorArgs
- struct CollectionExportArgs
- struct CollectionNotFound
- struct ExportArgs
- struct File
- struct FileNotFound
- struct FixLinksArgs
- struct Group
- struct GroupNewArgs
- struct Link
- struct NewPageArgs
- struct Page
- struct PageNotFound
- struct ReadContentArgs
- struct ScanArgs
- struct Session