package renderer import ( "fmt" "os" "path/filepath" "strings" "unicode/utf8" "dirmd/internal/tree" "dirmd/internal/walker" ) const fenceChars = "~~~" func Write(result *walker.Result, outputPath string, appendMode bool, singleFile bool) error { flags := os.O_CREATE | os.O_WRONLY if appendMode { flags |= os.O_APPEND } else { flags |= os.O_TRUNC } f, err := os.OpenFile(outputPath, flags, 0644) if err != nil { return fmt.Errorf("cannot open output file: %w", err) } defer f.Close() var sb strings.Builder // Determine the correct H1 path h1Path := result.AbsRoot if singleFile && len(result.Entries) > 0 { h1Path = filepath.Join(result.AbsRoot, result.Entries[0].RelPath) } fmt.Fprintf(&sb, "# %s\n\n", h1Path) if !singleFile { root := tree.Build(result.Entries) rootName := filepath.Base(result.AbsRoot) sb.WriteString("## Directory Tree\n\n") sb.WriteString(fenceChars + "\n") sb.WriteString(tree.Render(root, rootName)) sb.WriteString(fenceChars + "\n\n") } if len(result.Entries) > 0 { if !singleFile { sb.WriteString("## Contents\n\n") } for _, entry := range result.Entries { fullPath := filepath.Join(result.AbsRoot, entry.RelPath) content, err := readFile(fullPath) if err != nil { fmt.Fprintf(os.Stderr, "warning: cannot read %s: %v\n", entry.RelPath, err) continue } language := extToLang(filepath.Ext(entry.RelPath)) fmt.Fprintf(&sb, "### ./%s\n\n", entry.RelPath) fmt.Fprintf(&sb, "%s", fenceChars) if language != "" { fmt.Fprintf(&sb, "%s", language) } fmt.Fprintf(&sb, "\n") fmt.Fprintf(&sb, "%s", content) if !strings.HasSuffix(content, "\n") { fmt.Fprintf(&sb, "\n") } fmt.Fprintf(&sb, "%s\n\n", fenceChars) } } _, err = f.WriteString(sb.String()) return err } func readFile(path string) (string, error) { data, err := os.ReadFile(path) if err != nil { return "", err } for i, b := range data { if b == 0 { return "", fmt.Errorf("contains null byte at position %d", i) } } if !utf8.Valid(data) { return "", fmt.Errorf("invalid UTF-8 encoding") } content := string(data) content = strings.ReplaceAll(content, "\r\n", "\n") content = strings.ReplaceAll(content, "\r", "\n") return content, nil } func extToLang(ext string) string { mapping := map[string]string{ ".go": "go", ".md": "markdown", ".yml": "yaml", ".yaml": "yaml", ".js": "javascript", ".ts": "typescript", ".css": "css", ".html": "html", ".json": "json", ".xml": "xml", ".sql": "sql", ".sh": "bash", ".py": "python", ".rs": "rust", ".c": "c", ".h": "c", ".cpp": "cpp", ".hpp": "cpp", ".txt": "text", } if lang, ok := mapping[strings.ToLower(ext)]; ok { return lang } return "" }