...
This commit is contained in:
@@ -123,4 +123,49 @@ fn test_export_without_includes() {
|
|||||||
// Verify exported page1 still has include action
|
// Verify exported page1 still has include action
|
||||||
exported := os.read_file('${export_path}/test_col2/page1.md')!
|
exported := os.read_file('${export_path}/test_col2/page1.md')!
|
||||||
assert exported.contains('!!include')
|
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()
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ module atlas
|
|||||||
import incubaid.herolib.core.pathlib
|
import incubaid.herolib.core.pathlib
|
||||||
import incubaid.herolib.core.texttools
|
import incubaid.herolib.core.texttools
|
||||||
import incubaid.herolib.core.base
|
import incubaid.herolib.core.base
|
||||||
|
import incubaid.herolib.ui.console
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@[heap]
|
@[heap]
|
||||||
@@ -13,8 +14,9 @@ pub mut:
|
|||||||
pages map[string]&Page
|
pages map[string]&Page
|
||||||
images map[string]&File
|
images map[string]&File
|
||||||
files map[string]&File
|
files map[string]&File
|
||||||
atlas &Atlas @[skip; str: skip] // Reference to parent atlas for include resolution
|
atlas &Atlas @[skip; str: skip] // Reference to parent atlas for include resolution
|
||||||
errors []CollectionError
|
errors []CollectionError
|
||||||
|
error_cache map[string]bool // Track error hashes to avoid duplicates
|
||||||
}
|
}
|
||||||
|
|
||||||
@[params]
|
@[params]
|
||||||
@@ -30,9 +32,10 @@ fn (mut self Atlas) new_collection(args CollectionNewArgs) !Collection {
|
|||||||
mut path := pathlib.get_dir(path: args.path)!
|
mut path := pathlib.get_dir(path: args.path)!
|
||||||
|
|
||||||
mut col := Collection{
|
mut col := Collection{
|
||||||
name: name
|
name: name
|
||||||
path: path
|
path: path
|
||||||
atlas: &self // Set atlas reference
|
atlas: &self // Set atlas reference
|
||||||
|
error_cache: map[string]bool{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return col
|
return col
|
||||||
@@ -208,3 +211,87 @@ pub fn (mut c Collection) export(args CollectionExportArgs) ! {
|
|||||||
redis.hset('atlas:path', c.name, col_dir.path)!
|
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()}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,22 +1,61 @@
|
|||||||
module atlas
|
module atlas
|
||||||
|
|
||||||
|
import crypto.md5
|
||||||
|
import incubaid.herolib.ui.console
|
||||||
|
|
||||||
pub enum CollectionErrorCategory {
|
pub enum CollectionErrorCategory {
|
||||||
missing_include
|
circular_include
|
||||||
include_syntax_error
|
missing_include
|
||||||
circular_include
|
include_syntax_error
|
||||||
page_not_found
|
invalid_page_reference
|
||||||
file_not_found
|
file_not_found
|
||||||
collection_not_found
|
invalid_collection
|
||||||
other
|
general_error
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CollectionError {
|
pub struct CollectionError {
|
||||||
pub:
|
pub mut:
|
||||||
page_key string // "collection:page_name" if applicable
|
category CollectionErrorCategory
|
||||||
message string
|
page_key string // Format: "collection:page" or just collection name
|
||||||
category CollectionErrorCategory
|
message string
|
||||||
|
file string // Optional: specific file path if relevant
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn (e CollectionError) markdown() string {
|
// Generate MD5 hash for error deduplication
|
||||||
return 'ERROR [${e.category.str()}]: ${e.message}' + (if e.page_key != '' { ' (Page: `${e.page_key}`)' } else { '' })
|
// 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' }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -26,5 +26,10 @@ pub fn (mut a Atlas) export(args ExportArgs) ! {
|
|||||||
include: args.include
|
include: args.include
|
||||||
redis: args.redis
|
redis: args.redis
|
||||||
)!
|
)!
|
||||||
|
|
||||||
|
// Print errors for this collection if any
|
||||||
|
if col.has_errors() {
|
||||||
|
col.print_errors()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,6 @@ module atlas
|
|||||||
|
|
||||||
import incubaid.herolib.core.pathlib
|
import incubaid.herolib.core.pathlib
|
||||||
import incubaid.herolib.core.texttools
|
import incubaid.herolib.core.texttools
|
||||||
import incubaid.herolib.data.atlas.collection_error { CollectionError, CollectionErrorCategory }
|
|
||||||
|
|
||||||
@[heap]
|
@[heap]
|
||||||
pub struct Page {
|
pub struct Page {
|
||||||
@@ -59,12 +58,13 @@ fn (mut p Page) process_includes(content string, mut visited map[string]bool) !s
|
|||||||
// Prevent circular includes
|
// Prevent circular includes
|
||||||
page_key := p.key()
|
page_key := p.key()
|
||||||
if page_key in visited {
|
if page_key in visited {
|
||||||
p.collection.errors << CollectionError{
|
p.collection.error(
|
||||||
page_key: page_key
|
category: .circular_include
|
||||||
message: 'Circular include detected for page `${page_key}`.'
|
page_key: page_key
|
||||||
category: .circular_include
|
message: 'Circular include detected for page `${page_key}`'
|
||||||
}
|
show_console: false // Don't show immediately, collect for later
|
||||||
return '' // Return empty string for circular includes
|
)
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
visited[page_key] = true
|
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_collection = texttools.name_fix(parts[0])
|
||||||
target_page = texttools.name_fix(parts[1])
|
target_page = texttools.name_fix(parts[1])
|
||||||
} else {
|
} else {
|
||||||
p.collection.errors << CollectionError{
|
p.collection.error(
|
||||||
page_key: page_key
|
category: .include_syntax_error
|
||||||
message: 'Invalid include format: `${include_ref}`.'
|
page_key: page_key
|
||||||
category: .include_syntax_error
|
message: 'Invalid include format: `${include_ref}`'
|
||||||
}
|
show_console: false
|
||||||
|
)
|
||||||
processed_lines << '<!-- Invalid include format: ${include_ref} -->'
|
processed_lines << '<!-- Invalid include format: ${include_ref} -->'
|
||||||
continue
|
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
|
// Get the referenced page from atlas
|
||||||
mut include_page := atlas.page_get(page_ref) or {
|
mut include_page := atlas.page_get(page_ref) or {
|
||||||
p.collection.errors << CollectionError{
|
p.collection.error(
|
||||||
page_key: page_key
|
category: .missing_include
|
||||||
message: 'Included page `${page_ref}` not found.'
|
page_key: page_key
|
||||||
category: .missing_include
|
message: 'Included page `${page_ref}` not found'
|
||||||
}
|
show_console: false
|
||||||
// If page not found, keep original line as comment
|
)
|
||||||
processed_lines << '<!-- Include not found: ${page_ref} -->'
|
processed_lines << '<!-- Include not found: ${page_ref} -->'
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,96 +8,96 @@ import os
|
|||||||
@[params]
|
@[params]
|
||||||
pub struct ScanArgs {
|
pub struct ScanArgs {
|
||||||
pub mut:
|
pub mut:
|
||||||
path string @[required]
|
path string @[required]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan a directory for collections
|
// Scan a directory for collections
|
||||||
fn (mut a Atlas) scan_directory(mut dir pathlib.Path) ! {
|
fn (mut a Atlas) scan_directory(mut dir pathlib.Path) ! {
|
||||||
if !dir.is_dir() {
|
if !dir.is_dir() {
|
||||||
return error('Path is not a directory: ${dir.path}')
|
return error('Path is not a directory: ${dir.path}')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this directory is a collection
|
// Check if this directory is a collection
|
||||||
if is_collection_dir(dir) {
|
if is_collection_dir(dir) {
|
||||||
collection_name := get_collection_name(mut dir)!
|
collection_name := get_collection_name(mut dir)!
|
||||||
a.add_collection(path: dir.path, name: collection_name)!
|
a.add_collection(path: dir.path, name: collection_name)!
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan subdirectories
|
// Scan subdirectories
|
||||||
entries := dir.list(recursive: false)!
|
mut entries := dir.list(recursive: false)!
|
||||||
for entry in entries.paths {
|
for mut entry in entries.paths {
|
||||||
if !entry.is_dir() || should_skip_dir(entry) {
|
if !entry.is_dir() || should_skip_dir(entry) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
mut mutable_entry := entry
|
mut mutable_entry := entry
|
||||||
a.scan_directory(mut mutable_entry)!
|
a.scan_directory(mut mutable_entry)!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if directory is a collection
|
// Check if directory is a collection
|
||||||
fn is_collection_dir(path pathlib.Path) bool {
|
fn is_collection_dir(path pathlib.Path) bool {
|
||||||
return path.file_exists('.collection')
|
return path.file_exists('.collection')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get collection name from .collection file
|
// Get collection name from .collection file
|
||||||
fn get_collection_name(mut path pathlib.Path) !string {
|
fn get_collection_name(mut path pathlib.Path) !string {
|
||||||
mut collection_name := path.name()
|
mut collection_name := path.name()
|
||||||
mut filepath := path.file_get('.collection')!
|
mut filepath := path.file_get('.collection')!
|
||||||
|
|
||||||
content := filepath.read()!
|
content := filepath.read()!
|
||||||
if content.trim_space() != '' {
|
if content.trim_space() != '' {
|
||||||
mut params := paramsparser.parse(content)!
|
mut params := paramsparser.parse(content)!
|
||||||
if params.exists('name') {
|
if params.exists('name') {
|
||||||
collection_name = params.get('name')!
|
collection_name = params.get('name')!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return texttools.name_fix(collection_name)
|
return texttools.name_fix(collection_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if directory should be skipped
|
// Check if directory should be skipped
|
||||||
fn should_skip_dir(entry pathlib.Path) bool {
|
fn should_skip_dir(entry pathlib.Path) bool {
|
||||||
name := entry.name()
|
name := entry.name()
|
||||||
return name.starts_with('.') || name.starts_with('_')
|
return name.starts_with('.') || name.starts_with('_')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan collection directory for files
|
// Scan collection directory for files
|
||||||
fn (mut c Collection) scan() ! {
|
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) ! {
|
fn (mut c Collection) scan_path(mut dir pathlib.Path) ! {
|
||||||
entries := dir.list(recursive: false)!
|
mut entries := dir.list(recursive: false)!
|
||||||
|
|
||||||
for entry in entries.paths {
|
for mut entry in entries.paths {
|
||||||
// Skip hidden files/dirs
|
// Skip hidden files/dirs
|
||||||
if entry.name().starts_with('.') || entry.name().starts_with('_') {
|
if entry.name().starts_with('.') || entry.name().starts_with('_') {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if entry.is_dir() {
|
if entry.is_dir() {
|
||||||
// Recursively scan subdirectories
|
// Recursively scan subdirectories
|
||||||
mut mutable_entry := entry
|
mut mutable_entry := entry
|
||||||
c.scan_path(mut mutable_entry)!
|
c.scan_path(mut mutable_entry)!
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process files based on extension
|
// Process files based on extension
|
||||||
match entry.extension_lower() {
|
match entry.extension_lower() {
|
||||||
'md' {
|
'md' {
|
||||||
mut mutable_entry := entry
|
mut mutable_entry := entry
|
||||||
c.add_page(mut mutable_entry)!
|
c.add_page(mut mutable_entry)!
|
||||||
}
|
}
|
||||||
'png', 'jpg', 'jpeg', 'gif', 'svg' {
|
'png', 'jpg', 'jpeg', 'gif', 'svg' {
|
||||||
mut mutable_entry := entry
|
mut mutable_entry := entry
|
||||||
c.add_image(mut mutable_entry)!
|
c.add_image(mut mutable_entry)!
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
mut mutable_entry := entry
|
mut mutable_entry := entry
|
||||||
c.add_file(mut mutable_entry)!
|
c.add_file(mut mutable_entry)!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user