package tree import ( "fmt" "io/fs" "path/filepath" "strings" "git.flo.fo/FLO/dirmd/internal/config" "git.flo.fo/FLO/dirmd/internal/filter" ) func BuildFullTree(absRoot string, cfg *config.Config) (*Node, error) { root := &Node{IsDir: true, Children: make(map[string]*Node)} err := filepath.WalkDir(absRoot, func(path string, d fs.DirEntry, err error) error { if err != nil { return nil } rel, rerr := filepath.Rel(absRoot, path) if rerr != nil || rel == "." { return nil } if filter.IsSymlink(path) { return nil } depth := strings.Count(rel, string(filepath.Separator)) + 1 if depth > cfg.MaxDepth { node := addPath(root, rel, d.IsDir()) node.Annotation = "exceeded --max-depth" if d.IsDir() { return filepath.SkipDir } return nil } name := filepath.Base(path) if d.IsDir() { if isIgnoredName(name, cfg.Ignores) { node := addPath(root, rel, true) node.Annotation = "ignored (--ignore)" return filepath.SkipDir } if cfg.OmitTest && isTestDir(rel) { node := addPath(root, rel, true) node.Annotation = "tests omitted (--omit-test)" return filepath.SkipDir } addPath(root, rel, true) return nil } if annotation, ok := fileAnnotation(rel, name, cfg); ok { node := addPath(root, rel, false) node.Annotation = annotation return nil } addPath(root, rel, false) return nil }) if err != nil { return nil, fmt.Errorf("cannot walk %s: %w", absRoot, err) } return root, nil } func isIgnoredName(name string, ignores []string) bool { if strings.HasPrefix(name, ".") { return true } for _, pattern := range ignores { if matched, _ := filepath.Match(pattern, name); matched { return true } } return false } func isTestDir(rel string) bool { slashPath := filepath.ToSlash(rel) return strings.Contains(slashPath, "/tests/") || strings.HasPrefix(slashPath, "tests/") } func fileAnnotation(rel, name string, cfg *config.Config) (string, bool) { if strings.HasPrefix(name, ".") { return "", false } for _, pattern := range cfg.Ignores { if matched, _ := filepath.Match(pattern, name); matched { return "ignored (--ignore)", true } } if cfg.OmitTest && isTestFileName(rel, name) { return "tests omitted (--omit-test)", true } if cfg.OmitMd && isNonReadmeMarkdown(name) { return "omitted (--omit-md)", true } if cfg.FrontendOnly && !isFrontendOrReadme(rel, name) { return "omitted (--frontend-only)", true } if cfg.SkipFrontend && isFrontendExt(rel) { return "omitted (--skip-frontend)", true } return "", false } func isTestFileName(rel, name string) bool { ext := strings.ToLower(filepath.Ext(name)) switch ext { case ".go": return strings.HasSuffix(strings.ToLower(name), "_test.go") case ".java": base := strings.TrimSuffix(name, filepath.Ext(name)) if strings.HasSuffix(base, "Test") || strings.HasSuffix(base, "Tests") { return true } slashPath := filepath.ToSlash(rel) return strings.Contains(slashPath, "/src/test/") case ".py": lower := strings.ToLower(name) if strings.HasPrefix(lower, "test_") || strings.HasSuffix(lower, "_test.py") { return true } if strings.EqualFold(name, "conftest.py") { return true } return isTestDir(rel) } return false } func isNonReadmeMarkdown(name string) bool { ext := strings.ToLower(filepath.Ext(name)) if ext != ".md" && ext != ".markdown" { return false } return !strings.EqualFold(name, "README.md") } func isFrontendOrReadme(rel, name string) bool { if isFrontendExt(rel) { return true } return strings.EqualFold(name, "README.md") } func isFrontendExt(rel string) bool { ext := strings.ToLower(filepath.Ext(rel)) for _, fe := range frontendExts { if strings.EqualFold(ext, fe) { return true } } return false } var frontendExts = []string{ ".html", ".htm", ".gohtml", ".tmpl", ".css", ".scss", ".sass", ".less", ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".vue", ".svelte", }