flake-update-20260201
  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}