package docsite import ( "fmt" "slices" "strings" gast "github.com/yuin/goldmark/ast" ) // calloutTypes are the only `> [!TYPE]` callouts the theme renders. var calloutTypes = []string{"NOTE", "TIP", "WARNING"} // checkPolicy enforces the page content rules: // - every Go-lexer fence in a docs/ page carries src=, at any depth // (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, error) { var problems []Problem for _, d := range docs { if d.page == nil { continue } guide := d.page.Module == "" if guide { fences, err := collectFences(d.doc, d.body) if err != nil { return nil, err } for _, f := range fences { if _, hasSrc := ParseSrc(f.info); goLang(f.lang) && !hasSrc { problems = append(problems, Problem{File: d.file, Line: d.line + f.line, Rule: "snippet", Message: f.lang + " code block has no src= reference"}) } } } problems = append(problems, calloutProblems(d)...) if guide { problems = append(problems, headingProblems(d)...) } } return problems, nil } // calloutProblems reports blockquotes whose marker is not NOTE, TIP or // WARNING. calloutTransformer has already turned the known types into // callout nodes, so a remaining blockquote with a marker is unknown. // The marker is read from the AST, so a copy of it inside a code fence // is not a callout. func calloutProblems(d parsedDoc) []Problem { var problems []Problem _ = gast.Walk(d.doc, func(n gast.Node, entering bool) (gast.WalkStatus, error) { bq, ok := n.(*gast.Blockquote) if !entering || !ok { return gast.WalkContinue, nil } para, ok := bq.FirstChild().(*gast.Paragraph) if !ok || para.Lines().Len() == 0 { return gast.WalkContinue, nil } first := para.Lines().At(0) m := calloutLine.FindStringSubmatch(strings.TrimSpace(string(first.Value(d.body)))) if m == nil || slices.Contains(calloutTypes, m[1]) { return gast.WalkContinue, nil } problems = append(problems, Problem{File: d.file, Line: lineOf(d.body, first.Start, d.line), Rule: "callout", Message: fmt.Sprintf("unknown type %s (use NOTE, TIP or WARNING)", m[1])}) return gast.WalkContinue, nil }) 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 }