package docsite import ( "fmt" "regexp" "slices" "strings" gast "github.com/yuin/goldmark/ast" ) // calloutTypes are the only `> [!TYPE]` callouts the theme renders. var calloutTypes = []string{"NOTE", "TIP", "WARNING"} // calloutMarker matches the first line of a callout blockquote. var calloutMarker = regexp.MustCompile(`^\s{0,3}>\s?\[!([A-Za-z]+)\]\s*$`) // checkPolicy enforces the page content rules: // - every go fence in a docs/ page carries src= (module READMEs are // rendered as written, D-18); // - callouts are NOTE, TIP or WARNING; // - docs/ headings are plain ASCII text without links or code spans. func (s *site) checkPolicy(docs []parsedDoc) []Problem { var problems []Problem for _, d := range docs { if d.page == nil { continue } guide := d.page.Module == "" lines := strings.Split(string(d.body), "\n") fences := scanFences(lines) inFence := make([]bool, len(lines)) for _, f := range fences { end := f.close if end < 0 { end = len(lines) - 1 } for i := f.open; i <= end; i++ { inFence[i] = true } fields := strings.Fields(f.info) if guide && len(fields) > 0 && fields[0] == "go" && !slices.ContainsFunc(fields[1:], func(f string) bool { return strings.HasPrefix(f, "src=") }) { problems = append(problems, Problem{File: d.file, Line: d.line + f.open, Rule: "snippet", Message: "go code block has no src= reference"}) } } for i, line := range lines { if inFence[i] { continue } if m := calloutMarker.FindStringSubmatch(line); m != nil && !slices.Contains(calloutTypes, m[1]) { problems = append(problems, Problem{File: d.file, Line: d.line + i, Rule: "callout", Message: fmt.Sprintf("unknown type %s (use NOTE, TIP or WARNING)", m[1])}) } } if guide { problems = append(problems, headingProblems(d)...) } } return problems } const headingMessage = "headings must be plain ASCII text without links or code" // headingProblems reports headings with a link, image, code span or raw // HTML, or with non-ASCII text: their IDs would not be stable across the // site, Gitea and GitHub. func headingProblems(d parsedDoc) []Problem { var problems []Problem _ = gast.Walk(d.doc, func(n gast.Node, entering bool) (gast.WalkStatus, error) { h, ok := n.(*gast.Heading) if !entering || !ok { return gast.WalkContinue, nil } bad := false _ = gast.Walk(h, func(c gast.Node, entering bool) (gast.WalkStatus, error) { if !entering { return gast.WalkContinue, nil } switch t := c.(type) { case *gast.Link, *gast.Image, *gast.CodeSpan, *gast.RawHTML, *gast.AutoLink: bad = true return gast.WalkStop, nil case *gast.Text: for _, b := range t.Segment.Value(d.body) { if b >= 0x80 { bad = true return gast.WalkStop, nil } } } return gast.WalkContinue, nil }) if bad { off := -1 if h.Lines().Len() > 0 { off = h.Lines().At(0).Start } problems = append(problems, Problem{File: d.file, Line: lineOf(d.body, off, d.line), Rule: "heading", Message: headingMessage}) } return gast.WalkSkipChildren, nil }) return problems }