Files
herolib/lib/develop/codewalker/codewalker.v
2025-10-12 12:30:19 +03:00

220 lines
5.5 KiB
V

module codewalker
import incubaid.herolib.core.pathlib
pub struct CodeWalker {
pub mut:
ignorematcher IgnoreMatcher
errors []CWError
}
@[params]
pub struct FileMapArgs {
pub mut:
path string
content string
content_read bool = true // if we start from path, and this is on false then we don't read the content
}
// Public factory to parse the filemap-text format directly
pub fn (mut cw CodeWalker) parse(content string) !FileMap {
return cw.filemap_get_from_content(content)
}
pub fn (mut cw CodeWalker) filemap_get(args FileMapArgs) !FileMap {
if args.path != '' {
return cw.filemap_get_from_path(args.path, args.content_read)!
} else if args.content != '' {
return cw.filemap_get_from_content(args.content)!
} else {
return error('Either path or content must be provided to get FileMap')
}
}
// get the filemap from a path
fn (mut cw CodeWalker) filemap_get_from_path(path string, content_read bool) !FileMap {
mut dir := pathlib.get(path)
if !dir.exists() || !dir.is_dir() {
return error('Source directory "${path}" does not exist')
}
mut files := dir.list(ignore_default: false)!
mut fm := FileMap{
source: path
}
// collect ignore patterns from .gitignore and .heroignore files (recursively),
// and scope them to the directory where they were found
for mut p in files.paths {
if p.is_file() {
name := p.name()
if name == '.gitignore' || name == '.heroignore' {
content := p.read() or { '' }
if content != '' {
rel := p.path_relative(path) or { '' }
base_rel := if rel.contains('/') { rel.all_before_last('/') } else { '' }
cw.ignorematcher.add_content_with_base(base_rel, content)
}
}
}
}
for mut file in files.paths {
if file.is_file() {
name := file.name()
if name == '.gitignore' || name == '.heroignore' {
continue
}
relpath := file.path_relative(path)!
if cw.ignorematcher.is_ignored(relpath) {
continue
}
if content_read {
content := file.read()!
fm.content[relpath] = content
} else {
fm.content[relpath] = ''
}
}
}
return fm
}
// Parse a header line and return (kind, filename)
// kind: 'FILE' | 'FILECHANGE' | 'LEGACY' | 'END'
fn (mut cw CodeWalker) parse_header(line string, linenr int) !(string, string) {
if line == '===END===' {
return 'END', ''
}
if line.starts_with('===FILE:') && line.ends_with('===') {
name := line.trim_left('=').trim_right('=').all_after(':').trim_space()
if name.len < 1 {
cw.error('Invalid filename, < 1 chars.', linenr, 'filename_get', true)!
}
return 'FILE', name
}
if line.starts_with('===FILECHANGE:') && line.ends_with('===') {
name := line.trim_left('=').trim_right('=').all_after(':').trim_space()
if name.len < 1 {
cw.error('Invalid filename, < 1 chars.', linenr, 'filename_get', true)!
}
return 'FILECHANGE', name
}
// Legacy header: ===filename===
if line.starts_with('===') && line.ends_with('===') {
name := line.trim('=').trim_space()
if name == 'END' {
return 'END', ''
}
if name.len < 1 {
cw.error('Invalid filename, < 1 chars.', linenr, 'filename_get', true)!
}
return 'LEGACY', name
}
return '', ''
}
fn (mut cw CodeWalker) error(msg string, linenr int, category string, fail bool) ! {
cw.errors << CWError{
message: msg
linenr: linenr
category: category
}
if fail {
return error(msg)
}
}
// internal function to get the filename
fn (mut cw CodeWalker) parse_filename_get(line string, linenr int) !string {
parts := line.split('===')
if parts.len < 2 {
cw.error('Invalid filename line: ${line}.', linenr, 'filename_get', true)!
}
mut name := parts[1].trim_space()
if name.len < 2 {
cw.error('Invalid filename, < 2 chars: ${name}.', linenr, 'filename_get', true)!
}
return name
}
enum ParseState {
start
in_block
}
// Parse filemap content string
fn (mut cw CodeWalker) filemap_get_from_content(content string) !FileMap {
mut fm := FileMap{}
mut current_kind := '' // 'FILE' | 'FILECHANGE' | 'LEGACY'
mut filename := ''
mut block := []string{}
mut had_any_block := false
mut linenr := 0
for line in content.split_into_lines() {
linenr += 1
line2 := line.trim_space()
kind, name := cw.parse_header(line2, linenr)!
if kind == 'END' {
if filename == '' {
if had_any_block {
cw.error("Filename 'END' is reserved.", linenr, 'parse', true)!
} else {
cw.error('END found at start, not good.', linenr, 'parse', true)!
}
} else {
if current_kind == 'FILE' || current_kind == 'LEGACY' {
fm.content[filename] = block.join_lines()
} else if current_kind == 'FILECHANGE' {
fm.content_change[filename] = block.join_lines()
}
filename = ''
block = []string{}
current_kind = ''
}
continue
}
if kind in ['FILE', 'FILECHANGE', 'LEGACY'] {
// starting a new block header
if filename != '' {
if current_kind == 'FILE' || current_kind == 'LEGACY' {
fm.content[filename] = block.join_lines()
} else if current_kind == 'FILECHANGE' {
fm.content_change[filename] = block.join_lines()
}
}
filename = name
current_kind = kind
block = []string{}
had_any_block = true
continue
}
// Non-header line
if filename == '' {
if line2.len > 0 {
cw.error("Unexpected content before first file block: '${line}'.", linenr,
'parse', false)!
}
} else {
block << line
}
}
// EOF: flush current block if any
if filename != '' {
if current_kind == 'FILE' || current_kind == 'LEGACY' {
fm.content[filename] = block.join_lines()
} else if current_kind == 'FILECHANGE' {
fm.content_change[filename] = block.join_lines()
}
}
return fm
}