fedora-csb-system-manager
1package main
2
3import (
4 "bufio"
5 "fmt"
6 "os"
7 "path/filepath"
8 "regexp"
9 "strings"
10
11 "github.com/bmatcuk/doublestar/v4"
12)
13
14// ANSI color codes
15const (
16 colorReset = "\x1b[0m"
17 colorRed = "\x1b[31m"
18 colorGreen = "\x1b[32m"
19 colorYellow = "\x1b[33m"
20 colorCyan = "\x1b[36m"
21)
22
23type brokenLink struct {
24 file string
25 link string
26 target string
27 line int
28}
29
30// extractLinks extracts markdown links from content
31func extractLinks(content string) []struct {
32 link string
33 line int
34} {
35 var links []struct {
36 link string
37 line int
38 }
39
40 // Match [text](path) style links
41 linkRegex := regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`)
42
43 scanner := bufio.NewScanner(strings.NewReader(content))
44 lineNum := 0
45 for scanner.Scan() {
46 lineNum++
47 line := scanner.Text()
48
49 matches := linkRegex.FindAllStringSubmatch(line, -1)
50 for _, match := range matches {
51 if len(match) >= 3 {
52 link := match[2]
53
54 // Skip external URLs, anchors, and mailto links
55 if strings.HasPrefix(link, "http://") ||
56 strings.HasPrefix(link, "https://") ||
57 strings.HasPrefix(link, "#") ||
58 strings.HasPrefix(link, "mailto:") {
59 continue
60 }
61
62 links = append(links, struct {
63 link string
64 line int
65 }{link: link, line: lineNum})
66 }
67 }
68 }
69
70 return links
71}
72
73// resolveLink resolves a link path relative to the file
74func resolveLink(fromFile, linkPath, baseDir string) string {
75 // Remove anchor if present
76 parts := strings.Split(linkPath, "#")
77 pathWithoutAnchor := parts[0]
78
79 // If it starts with ~, expand to home directory
80 if strings.HasPrefix(pathWithoutAnchor, "~/") {
81 home, err := os.UserHomeDir()
82 if err != nil {
83 return pathWithoutAnchor
84 }
85 return filepath.Join(home, pathWithoutAnchor[2:])
86 }
87
88 // If it's absolute, use as-is
89 if filepath.IsAbs(pathWithoutAnchor) {
90 return pathWithoutAnchor
91 }
92
93 // Otherwise, resolve relative to the file's directory
94 fileDir := filepath.Dir(fromFile)
95 return filepath.Join(fileDir, pathWithoutAnchor)
96}
97
98// validateDocs validates markdown files in a directory
99func validateDocs(baseDir string) []brokenLink {
100 var broken []brokenLink
101
102 // Find all markdown files using doublestar glob
103 pattern := filepath.Join(baseDir, "**/*.md")
104 matches, err := doublestar.FilepathGlob(pattern)
105 if err != nil {
106 fmt.Fprintf(os.Stderr, "%sError globbing files: %v%s\n", colorYellow, err, colorReset)
107 return broken
108 }
109
110 for _, filePath := range matches {
111 // Skip node_modules and hidden directories
112 if strings.Contains(filePath, "node_modules") || strings.Contains(filePath, "/.") {
113 continue
114 }
115
116 content, err := os.ReadFile(filePath)
117 if err != nil {
118 fmt.Fprintf(os.Stderr, "%sWarning: Could not read %s%s\n", colorYellow, filePath, colorReset)
119 continue
120 }
121
122 links := extractLinks(string(content))
123 for _, linkInfo := range links {
124 targetPath := resolveLink(filePath, linkInfo.link, baseDir)
125
126 // Check if target exists
127 if _, err := os.Stat(targetPath); os.IsNotExist(err) {
128 // Get relative path for display
129 relPath, _ := filepath.Rel(baseDir, filePath)
130 broken = append(broken, brokenLink{
131 file: relPath,
132 link: linkInfo.link,
133 target: targetPath,
134 line: linkInfo.line,
135 })
136 }
137 }
138 }
139
140 return broken
141}
142
143func main() {
144 baseDir, err := os.Getwd()
145 if err != nil {
146 fmt.Fprintf(os.Stderr, "Error getting current directory: %v\n", err)
147 os.Exit(1)
148 }
149
150 fmt.Printf("\n%sš Documentation Link Validator%s\n", colorCyan, colorReset)
151 fmt.Printf("%s Base directory: %s%s\n\n", colorCyan, baseDir, colorReset)
152
153 brokenLinks := validateDocs(baseDir)
154
155 if len(brokenLinks) > 0 {
156 fmt.Printf("\n%sā Found %d broken link(s):%s\n\n", colorRed, len(brokenLinks), colorReset)
157
158 for _, broken := range brokenLinks {
159 fmt.Printf(" %s%s:%d%s\n", colorYellow, broken.file, broken.line, colorReset)
160 fmt.Printf(" ā %s%s%s (not found)\n\n", colorRed, broken.link, colorReset)
161 }
162
163 fmt.Printf("\n%sDocumentation validation failed. Please fix the broken links.%s\n\n", colorRed, colorReset)
164 os.Exit(1)
165 }
166
167 fmt.Printf("%sā
All documentation links are valid%s\n\n", colorGreen, colorReset)
168 os.Exit(0)
169}