feat(11.1-02): check links, commands, forbidden names and fence policy
- relative links and anchors resolve against the renderer's heading IDs - summer and ./bin/<app> command names come from the real command constructors through docsite.Options.Commands; a nil set is a problem - consuming-application names fail in page sources and built outputs - go fences in docs/ pages need src=, callouts are NOTE, TIP or WARNING, docs/ headings are plain ASCII - gate gains --claude and self-test plants for each new rule
This commit is contained in:
66
internal/docsite/check_forbidden.go
Normal file
66
internal/docsite/check_forbidden.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package docsite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// forbiddenName matches the consuming-application spellings framework docs
|
||||
// must never contain (D-11), the accented variant included.
|
||||
var forbiddenName = regexp.MustCompile(`(?i)fonoteka|p[lł][yý]tarium`)
|
||||
|
||||
const forbiddenMessage = "consuming-application name in output"
|
||||
|
||||
// forbiddenLines returns the 1-based line numbers of text that name a
|
||||
// consuming application. The match itself is never reported.
|
||||
func forbiddenLines(text []byte) []int {
|
||||
var lines []int
|
||||
for i, line := range strings.Split(string(text), "\n") {
|
||||
if forbiddenName.MatchString(line) {
|
||||
lines = append(lines, i+1)
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
// checkForbiddenSources scans the full source file of every page (the
|
||||
// frontmatter and a README's summary line included).
|
||||
func (s *site) checkForbiddenSources() ([]Problem, error) {
|
||||
var problems []Problem
|
||||
for _, p := range s.pages {
|
||||
raw, err := os.ReadFile(p.abs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("docsite: read %s: %w", p.Source, err)
|
||||
}
|
||||
for _, line := range forbiddenLines(raw) {
|
||||
problems = append(problems, Problem{File: p.Source, Line: line, Rule: "forbidden", Message: forbiddenMessage})
|
||||
}
|
||||
}
|
||||
return problems, nil
|
||||
}
|
||||
|
||||
// forbiddenOutputExts are the text outputs scanned after rendering.
|
||||
var forbiddenOutputExts = []string{".html", ".md", ".txt", ".json", ".js", ".css"}
|
||||
|
||||
// checkForbiddenOutputs scans every rendered text output, which covers
|
||||
// text pulled in through src= snippet copies and templates.
|
||||
func (s *site) checkForbiddenOutputs() []Problem {
|
||||
names := make([]string, 0, len(s.outputs))
|
||||
for name := range s.outputs {
|
||||
if slices.Contains(forbiddenOutputExts, path.Ext(name)) {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
slices.Sort(names)
|
||||
var problems []Problem
|
||||
for _, name := range names {
|
||||
for _, line := range forbiddenLines(s.outputs[name]) {
|
||||
problems = append(problems, Problem{File: name, Line: line, Rule: "forbidden", Message: forbiddenMessage})
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
Reference in New Issue
Block a user