Prepare source release validation
This commit is contained in:
151
documentation_test.go
Normal file
151
documentation_test.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package configkit
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var markdownLinkPattern = regexp.MustCompile(`(?m)(?:^|[[:space:]])\[[^\]]*\]\(([^)[:space:]]+)`)
|
||||
|
||||
func TestLocalDocumentationLinks(t *testing.T) {
|
||||
repository, err := filepath.Abs(".")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var failures []string
|
||||
err = filepath.WalkDir(repository, func(path string, entry os.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if entry.IsDir() {
|
||||
if entry.Name() == ".git" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if filepath.Ext(path) != ".md" {
|
||||
return nil
|
||||
}
|
||||
content, readErr := os.ReadFile(path)
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
for _, match := range markdownLinkPattern.FindAllStringSubmatch(string(content), -1) {
|
||||
target := strings.Trim(match[1], "<>")
|
||||
if strings.Contains(target, "://") || strings.HasPrefix(target, "mailto:") {
|
||||
continue
|
||||
}
|
||||
filePart, fragment, _ := strings.Cut(target, "#")
|
||||
decoded, decodeErr := url.PathUnescape(filePart)
|
||||
if decodeErr != nil {
|
||||
failures = append(failures, fmt.Sprintf("%s: invalid escaped link %q", relativePath(repository, path), target))
|
||||
continue
|
||||
}
|
||||
linked := path
|
||||
if decoded != "" {
|
||||
linked = filepath.Clean(filepath.Join(filepath.Dir(path), filepath.FromSlash(decoded)))
|
||||
}
|
||||
_, statErr := os.Stat(linked)
|
||||
outside := linked != repository && !strings.HasPrefix(linked, repository+string(filepath.Separator))
|
||||
if os.IsNotExist(statErr) && outside && optionalSiblingSource(repository, linked) {
|
||||
continue // Recorded sibling-source links are optional in isolated clones.
|
||||
}
|
||||
if statErr != nil {
|
||||
failures = append(failures, fmt.Sprintf("%s: missing local link %q", relativePath(repository, path), target))
|
||||
continue
|
||||
}
|
||||
if fragment != "" && filepath.Ext(linked) == ".md" && !markdownHasFragment(linked, fragment) {
|
||||
failures = append(failures, fmt.Sprintf("%s: missing fragment in %q", relativePath(repository, path), target))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(failures) != 0 {
|
||||
t.Fatalf("documentation link failures:\n%s", strings.Join(failures, "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
func optionalSiblingSource(repository, linked string) bool {
|
||||
relative, err := filepath.Rel(filepath.Dir(repository), linked)
|
||||
if err != nil || relative == "." || filepath.IsAbs(relative) {
|
||||
return false
|
||||
}
|
||||
parts := strings.Split(filepath.ToSlash(relative), "/")
|
||||
if len(parts) == 0 {
|
||||
return false
|
||||
}
|
||||
switch strings.ToLower(parts[0]) {
|
||||
case "narratio", "notarius", "weatherreporter", "seriatim":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func markdownHasFragment(path, wanted string) bool {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer file.Close()
|
||||
wanted, err = url.PathUnescape(wanted)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
seen := make(map[string]int)
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if !strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
heading := strings.TrimSpace(strings.TrimLeft(line, "#"))
|
||||
base := markdownFragment(heading)
|
||||
count := seen[base]
|
||||
seen[base]++
|
||||
fragment := base
|
||||
if count > 0 {
|
||||
fragment = fmt.Sprintf("%s-%d", base, count)
|
||||
}
|
||||
if fragment == wanted {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func markdownFragment(heading string) string {
|
||||
var result strings.Builder
|
||||
previousSpace := false
|
||||
for _, character := range strings.ToLower(heading) {
|
||||
switch {
|
||||
case unicode.IsLetter(character), unicode.IsDigit(character), character == '_', character == '-':
|
||||
result.WriteRune(character)
|
||||
previousSpace = false
|
||||
case unicode.IsSpace(character):
|
||||
if result.Len() > 0 && !previousSpace {
|
||||
result.WriteByte('-')
|
||||
previousSpace = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.TrimRight(result.String(), "-")
|
||||
}
|
||||
|
||||
func relativePath(root, path string) string {
|
||||
relative, err := filepath.Rel(root, path)
|
||||
if err != nil {
|
||||
return path
|
||||
}
|
||||
return filepath.ToSlash(relative)
|
||||
}
|
||||
Reference in New Issue
Block a user