github.com/quay/claircore@v1.5.28/internal/cmd/mdbook-injecturls/main.go (about)

     1  // Mdbook-injecturls is a helper meant to collect urls via a comment directive.
     2  //
     3  // Any string declaration with a directive like
     4  //
     5  //	//doc:url <keyword>
     6  //
     7  // Will get added to a list and slip-streamed into the documentation where
     8  // there's a preprocessor directive like
     9  //
    10  //	{{# injecturls <keyword> }}
    11  //
    12  // Only go files are searched for directives. Any print verbs will be replaced with
    13  // asterisks in the output.
    14  package main
    15  
    16  import (
    17  	"context"
    18  	"fmt"
    19  	"go/ast"
    20  	"go/parser"
    21  	"go/token"
    22  	"io/fs"
    23  	"log"
    24  	"path/filepath"
    25  	"regexp"
    26  	"strconv"
    27  	"strings"
    28  
    29  	"github.com/quay/claircore/internal/mdbook"
    30  )
    31  
    32  var (
    33  	marker    = regexp.MustCompile(`\{\{#\s*injecturls\s(.+)\}\}`)
    34  	printverb = regexp.MustCompile(`%[+#]*[a-z]`)
    35  )
    36  
    37  func main() {
    38  	mdbook.Main("injecturls", newProc)
    39  }
    40  
    41  func newProc(ctx context.Context, cfg *mdbook.Context) (*mdbook.Proc, error) {
    42  	proc := mdbook.Proc{
    43  		Chapter: func(ctx context.Context, b *strings.Builder, c *mdbook.Chapter) error {
    44  			if c.Path == nil {
    45  				return nil
    46  			}
    47  			if !marker.MatchString(c.Content) {
    48  				return nil
    49  			}
    50  			ms := marker.FindStringSubmatch(c.Content)
    51  			if ct := len(ms); ct != 2 {
    52  				return fmt.Errorf("unexpected number of arguments: %d", ct)
    53  			}
    54  			keyword := strings.TrimSpace(ms[1])
    55  			log.Println("injecting urls into:", *c.Path)
    56  			var collect []string
    57  			inspectFunc := func(n ast.Node) bool {
    58  				decl, ok := n.(*ast.GenDecl)
    59  				if !ok || (decl.Tok != token.CONST && decl.Tok != token.VAR) {
    60  					return true
    61  				}
    62  				collectblock := false
    63  				if decl.Doc != nil {
    64  					for _, c := range decl.Doc.List {
    65  						if !strings.Contains(c.Text, "//doc:url") {
    66  							continue
    67  						}
    68  						argv := strings.Fields(c.Text)
    69  						if len(argv) != 2 {
    70  							continue
    71  						}
    72  						if want, got := keyword, strings.TrimSpace(argv[1]); got != want {
    73  							continue
    74  						}
    75  						collectblock = true
    76  					}
    77  				}
    78  				for _, vs := range decl.Specs {
    79  					v, ok := vs.(*ast.ValueSpec)
    80  					if !ok {
    81  						continue
    82  					}
    83  					if !collectblock {
    84  						if v.Doc == nil {
    85  							continue
    86  						}
    87  						for _, c := range v.Doc.List {
    88  							if !strings.Contains(c.Text, "//doc:url") {
    89  								continue
    90  							}
    91  							argv := strings.Fields(c.Text)
    92  							if len(argv) != 2 {
    93  								continue
    94  							}
    95  							if want, got := keyword, strings.TrimSpace(argv[1]); got != want {
    96  								continue
    97  							}
    98  							goto Collect
    99  						}
   100  						continue
   101  					}
   102  				Collect:
   103  					for _, v := range v.Values {
   104  						lit, ok := v.(*ast.BasicLit)
   105  						if !ok {
   106  							continue
   107  						}
   108  						if lit.Kind != token.STRING {
   109  							continue
   110  						}
   111  						collect = append(collect, lit.Value)
   112  					}
   113  				}
   114  				return true
   115  			}
   116  			walkFunc := func(p string, d fs.DirEntry, err error) error {
   117  				if err != nil {
   118  					return err
   119  				}
   120  				if d.IsDir() || !strings.HasSuffix(p, ".go") {
   121  					return nil
   122  				}
   123  				fset := token.NewFileSet()
   124  				f, err := parser.ParseFile(fset, p, nil, parser.ParseComments|parser.SkipObjectResolution)
   125  				if err != nil {
   126  					return err
   127  				}
   128  				ast.Inspect(f, inspectFunc)
   129  				return nil
   130  			}
   131  			if err := filepath.WalkDir(cfg.Root, walkFunc); err != nil {
   132  				return err
   133  			}
   134  
   135  			for i, in := range collect {
   136  				if i == 0 {
   137  					b.WriteString("<ul>\n")
   138  				}
   139  				s, err := strconv.Unquote(in)
   140  				if err != nil {
   141  					return err
   142  				}
   143  				b.WriteString("<li>")
   144  				b.WriteString(printverb.ReplaceAllLiteralString(s, `&ast;`))
   145  				b.WriteString("</li>\n")
   146  			}
   147  			if b.Len() != 0 {
   148  				b.WriteString("</ul>\n")
   149  			}
   150  
   151  			c.Content = marker.ReplaceAllLiteralString(c.Content, b.String())
   152  			return nil
   153  		},
   154  	}
   155  	return &proc, nil
   156  }