This commit is contained in:
2025-10-16 09:51:42 +04:00
parent 91fdf9a774
commit 099b21510d
6 changed files with 283 additions and 106 deletions

View File

@@ -123,4 +123,49 @@ fn test_export_without_includes() {
// 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
}
fn test_error_hash() {
err1 := CollectionError{
category: .missing_include
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()
}

View File

@@ -3,6 +3,7 @@ module atlas
import incubaid.herolib.core.pathlib
import incubaid.herolib.core.texttools
import incubaid.herolib.core.base
import incubaid.herolib.ui.console
import os
@[heap]
@@ -13,8 +14,9 @@ pub mut:
pages map[string]&Page
images map[string]&File
files map[string]&File
atlas &Atlas @[skip; str: skip] // Reference to parent atlas for include resolution
errors []CollectionError
atlas &Atlas @[skip; str: skip] // Reference to parent atlas for include resolution
errors []CollectionError
error_cache map[string]bool // Track error hashes to avoid duplicates
}
@[params]
@@ -30,9 +32,10 @@ fn (mut self Atlas) new_collection(args CollectionNewArgs) !Collection {
mut path := pathlib.get_dir(path: args.path)!
mut col := Collection{
name: name
path: path
atlas: &self // Set atlas reference
name: name
path: path
atlas: &self // Set atlas reference
error_cache: map[string]bool{}
}
return col
@@ -208,3 +211,87 @@ pub fn (mut c Collection) export(args CollectionExportArgs) ! {
redis.hset('atlas:path', c.name, col_dir.path)!
}
}
@[params]
pub 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)
}
// Report an error, avoiding duplicates based on hash
pub fn (mut c Collection) error(args CollectionErrorArgs) {
// Create error struct
err := CollectionError{
category: args.category
page_key: args.page_key
message: args.message
file: args.file
}
// Calculate hash for deduplication
hash := err.hash()
// Check if this error was already reported
if hash in c.error_cache {
return // Skip duplicate
}
// Mark this error as reported
c.error_cache[hash] = true
// Log to errors array if requested
if args.log_error {
c.errors << err
}
// Show in console if requested
if args.show_console {
console.print_stderr('[${c.name}] ${err.str()}')
}
}
// Get all errors
pub fn (c Collection) get_errors() []CollectionError {
return c.errors
}
// Check if collection has errors
pub fn (c Collection) has_errors() bool {
return c.errors.len > 0
}
// Clear all errors
pub fn (mut c Collection) clear_errors() {
c.errors = []CollectionError{}
c.error_cache = map[string]bool{}
}
// Get error summary by category
pub fn (c Collection) error_summary() map[CollectionErrorCategory]int {
mut summary := map[CollectionErrorCategory]int{}
for err in c.errors {
summary[err.category] = summary[err.category] + 1
}
return summary
}
// Print all errors to console
pub fn (c Collection) print_errors() {
if c.errors.len == 0 {
console.print_green('Collection ${c.name}: No errors')
return
}
console.print_header('Collection ${c.name} - Errors (${c.errors.len})')
for err in c.errors {
console.print_stderr(' ${err.str()}')
}
}

View File

@@ -1,22 +1,61 @@
module atlas
import crypto.md5
import incubaid.herolib.ui.console
pub enum CollectionErrorCategory {
missing_include
include_syntax_error
circular_include
page_not_found
file_not_found
collection_not_found
other
circular_include
missing_include
include_syntax_error
invalid_page_reference
file_not_found
invalid_collection
general_error
}
pub struct CollectionError {
pub:
page_key string // "collection:page_name" if applicable
message string
category CollectionErrorCategory
pub mut:
category CollectionErrorCategory
page_key string // Format: "collection:page" or just collection name
message string
file string // Optional: specific file path if relevant
}
pub fn (e CollectionError) markdown() string {
return 'ERROR [${e.category.str()}]: ${e.message}' + (if e.page_key != '' { ' (Page: `${e.page_key}`)' } else { '' })
// Generate MD5 hash for error deduplication
// Hash is based on category + page_key (or file if page_key is empty)
pub fn (e CollectionError) hash() string {
mut hash_input := '${e.category}'
if e.page_key != '' {
hash_input += ':${e.page_key}'
} else if e.file != '' {
hash_input += ':${e.file}'
}
return md5.hexhash(hash_input)
}
// Get human-readable error message
pub fn (e CollectionError) str() string {
mut location := ''
if e.page_key != '' {
location = ' [${e.page_key}]'
} else if e.file != '' {
location = ' [${e.file}]'
}
return '[${e.category}]${location}: ${e.message}'
}
// Get category as string
pub fn (e CollectionError) category_str() string {
return match e.category {
.circular_include { 'Circular Include' }
.missing_include { 'Missing Include' }
.include_syntax_error { 'Include Syntax Error' }
.invalid_page_reference { 'Invalid Page Reference' }
.file_not_found { 'File Not Found' }
.invalid_collection { 'Invalid Collection' }
.general_error { 'General Error' }
}
}

View File

@@ -26,5 +26,10 @@ pub fn (mut a Atlas) export(args ExportArgs) ! {
include: args.include
redis: args.redis
)!
// Print errors for this collection if any
if col.has_errors() {
col.print_errors()
}
}
}

View File

@@ -2,7 +2,6 @@ module atlas
import incubaid.herolib.core.pathlib
import incubaid.herolib.core.texttools
import incubaid.herolib.data.atlas.collection_error { CollectionError, CollectionErrorCategory }
@[heap]
pub struct Page {
@@ -59,12 +58,13 @@ fn (mut p Page) process_includes(content string, mut visited map[string]bool) !s
// Prevent circular includes
page_key := p.key()
if page_key in visited {
p.collection.errors << CollectionError{
page_key: page_key
message: 'Circular include detected for page `${page_key}`.'
category: .circular_include
}
return '' // Return empty string for circular includes
p.collection.error(
category: .circular_include
page_key: page_key
message: 'Circular include detected for page `${page_key}`'
show_console: false // Don't show immediately, collect for later
)
return ''
}
visited[page_key] = true
@@ -90,11 +90,12 @@ fn (mut p Page) process_includes(content string, mut visited map[string]bool) !s
target_collection = texttools.name_fix(parts[0])
target_page = texttools.name_fix(parts[1])
} else {
p.collection.errors << CollectionError{
page_key: page_key
message: 'Invalid include format: `${include_ref}`.'
category: .include_syntax_error
}
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
}
@@ -112,12 +113,12 @@ fn (mut p Page) process_includes(content string, mut visited map[string]bool) !s
// Get the referenced page from atlas
mut include_page := atlas.page_get(page_ref) or {
p.collection.errors << CollectionError{
page_key: page_key
message: 'Included page `${page_ref}` not found.'
category: .missing_include
}
// If page not found, keep original line as comment
p.collection.error(
category: .missing_include
page_key: page_key
message: 'Included page `${page_ref}` not found'
show_console: false
)
processed_lines << '<!-- Include not found: ${page_ref} -->'
continue
}

View File

@@ -8,96 +8,96 @@ import os
@[params]
pub struct ScanArgs {
pub mut:
path string @[required]
path string @[required]
}
// Scan a directory for collections
fn (mut a Atlas) scan_directory(mut dir pathlib.Path) ! {
if !dir.is_dir() {
return error('Path is not a directory: ${dir.path}')
}
// Check if this directory is a collection
if is_collection_dir(dir) {
collection_name := get_collection_name(mut dir)!
a.add_collection(path: dir.path, name: collection_name)!
return
}
// Scan subdirectories
entries := dir.list(recursive: false)!
for entry in entries.paths {
if !entry.is_dir() || should_skip_dir(entry) {
continue
}
mut mutable_entry := entry
a.scan_directory(mut mutable_entry)!
}
if !dir.is_dir() {
return error('Path is not a directory: ${dir.path}')
}
// Check if this directory is a collection
if is_collection_dir(dir) {
collection_name := get_collection_name(mut dir)!
a.add_collection(path: dir.path, name: collection_name)!
return
}
// Scan subdirectories
mut entries := dir.list(recursive: false)!
for mut entry in entries.paths {
if !entry.is_dir() || should_skip_dir(entry) {
continue
}
mut mutable_entry := entry
a.scan_directory(mut mutable_entry)!
}
}
// Check if directory is a collection
fn is_collection_dir(path pathlib.Path) bool {
return path.file_exists('.collection')
return path.file_exists('.collection')
}
// Get collection name from .collection file
fn get_collection_name(mut path pathlib.Path) !string {
mut collection_name := path.name()
mut filepath := path.file_get('.collection')!
content := filepath.read()!
if content.trim_space() != '' {
mut params := paramsparser.parse(content)!
if params.exists('name') {
collection_name = params.get('name')!
}
}
return texttools.name_fix(collection_name)
mut collection_name := path.name()
mut filepath := path.file_get('.collection')!
content := filepath.read()!
if content.trim_space() != '' {
mut params := paramsparser.parse(content)!
if params.exists('name') {
collection_name = params.get('name')!
}
}
return texttools.name_fix(collection_name)
}
// Check if directory should be skipped
fn should_skip_dir(entry pathlib.Path) bool {
name := entry.name()
return name.starts_with('.') || name.starts_with('_')
name := entry.name()
return name.starts_with('.') || name.starts_with('_')
}
// Scan collection directory for files
fn (mut c Collection) scan() ! {
c.scan_path(mut c.path)!
c.scan_path(mut c.path)!
}
fn (mut c Collection) scan_path(mut dir pathlib.Path) ! {
entries := dir.list(recursive: false)!
for 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_path(mut mutable_entry)!
continue
}
// Process files based on extension
match entry.extension_lower() {
'md' {
mut mutable_entry := entry
c.add_page(mut mutable_entry)!
}
'png', 'jpg', 'jpeg', 'gif', 'svg' {
mut mutable_entry := entry
c.add_image(mut mutable_entry)!
}
else {
mut mutable_entry := entry
c.add_file(mut mutable_entry)!
}
}
}
}
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_path(mut mutable_entry)!
continue
}
// Process files based on extension
match entry.extension_lower() {
'md' {
mut mutable_entry := entry
c.add_page(mut mutable_entry)!
}
'png', 'jpg', 'jpeg', 'gif', 'svg' {
mut mutable_entry := entry
c.add_image(mut mutable_entry)!
}
else {
mut mutable_entry := entry
c.add_file(mut mutable_entry)!
}
}
}
}