//go:build xr package main // xr reads source files and rfc files and generates html versions, a code and // rfc index file, and an overal index file to view code and rfc side by side. import ( "bytes" "flag" "fmt" htmltemplate "html/template" "log" "os" "path/filepath" "regexp" "sort" "strconv" "strings" "golang.org/x/exp/maps" ) var destdir string func xcheckf(err error, format string, args ...any) { if err != nil { log.Fatalf("%s: %s", fmt.Sprintf(format, args...), err) } } func xwritefile(path string, buf []byte) { p := filepath.Join(destdir, path) os.MkdirAll(filepath.Dir(p), 0755) err := os.WriteFile(p, buf, 0644) xcheckf(err, "writing file %s", p) } func main() { log.SetFlags(0) var release bool flag.BoolVar(&release, "release", false, "generate cross-references for a release, highlighting the release version as active page") flag.Usage = func() { log.Println("usage: go run xr.go destdir revision date latestrelease ../*.go ../*/*.go") flag.PrintDefaults() os.Exit(2) } flag.Parse() args := flag.Args() if len(args) < 4 { flag.Usage() } destdir = args[0] revision := args[1] date := args[2] latestRelease := args[3] srcfiles := args[4:] // Generate code.html index. srcdirs := map[string][]string{} for _, arg := range srcfiles { arg = strings.TrimPrefix(arg, "../") dir := filepath.Dir(arg) file := filepath.Base(arg) srcdirs[dir] = append(srcdirs[dir], file) } for _, files := range srcdirs { sort.Strings(files) } dirs := maps.Keys(srcdirs) sort.Strings(dirs) var codeBuf bytes.Buffer err := codeTemplate.Execute(&codeBuf, map[string]any{ "Dirs": srcdirs, }) xcheckf(err, "generating code.html") xwritefile("code.html", codeBuf.Bytes()) // Generate code html files. re := regexp.MustCompile(`(\.\./)?rfc/[0-9]{3,5}(-[^ :]*)?(:[0-9]+)?`) for dir, files := range srcdirs { for _, file := range files { src := filepath.Join("..", dir, file) dst := filepath.Join(dir, file+".html") buf, err := os.ReadFile(src) xcheckf(err, "reading file %s", src) var b bytes.Buffer fmt.Fprint(&b, ` `) for i, line := range strings.Split(string(buf), "\n") { n := i + 1 _, err := fmt.Fprintf(&b, `
%d`, n, n, n) xcheckf(err, "writing source line") if line == "" { b.WriteString("\n") } else { for line != "" { loc := re.FindStringIndex(line) if loc == nil { b.WriteString(htmltemplate.HTMLEscapeString(line)) break } s, e := loc[0], loc[1] b.WriteString(htmltemplate.HTMLEscapeString(line[:s])) match := line[s:e] line = line[e:] t := strings.Split(match, ":") linenumber := 1 if len(t) == 2 { v, err := strconv.ParseInt(t[1], 10, 31) xcheckf(err, "parsing linenumber %q", t[1]) linenumber = int(v) } fmt.Fprintf(&b, `%s`, t[0], linenumber, htmltemplate.HTMLEscapeString(match)) } } fmt.Fprint(&b, "
\n") } fmt.Fprint(&b, ` `) xwritefile(dst, b.Bytes()) } } // Generate rfc index. rfctext, err := os.ReadFile("index.txt") xcheckf(err, "reading rfc index.txt") type rfc struct { File string Title string } topics := map[string][]rfc{} var topic string for _, line := range strings.Split(string(rfctext), "\n") { if strings.HasPrefix(line, "# ") { topic = line[2:] continue } t := strings.Split(line, "\t") if len(t) != 4 { continue } topics[topic] = append(topics[topic], rfc{strings.TrimSpace(t[0]), t[3]}) } for _, l := range topics { sort.Slice(l, func(i, j int) bool { return l[i].File < l[j].File }) } var rfcBuf bytes.Buffer err = rfcTemplate.Execute(&rfcBuf, map[string]any{ "Topics": topics, }) xcheckf(err, "generating rfc.html") xwritefile("rfc.html", rfcBuf.Bytes()) // Process each rfc file into html. for _, rfcs := range topics { for _, rfc := range rfcs { dst := filepath.Join("rfc", rfc.File+".html") buf, err := os.ReadFile(rfc.File) xcheckf(err, "reading rfc %s", rfc.File) var b bytes.Buffer fmt.Fprint(&b, ` `) isRef := func(s string) bool { return s[0] >= '0' && s[0] <= '9' || strings.HasPrefix(s, "../") } parseRef := func(s string) (string, int, bool) { t := strings.Split(s, ":") linenumber := 1 if len(t) == 2 { v, err := strconv.ParseInt(t[1], 10, 31) xcheckf(err, "parsing linenumber") linenumber = int(v) } isCode := strings.HasPrefix(t[0], "../") return t[0], linenumber, isCode } for i, line := range strings.Split(string(buf), "\n") { if line == "" { line = "\n" } else if len(line) < 80 || strings.Contains(rfc.File, "-") && i > 0 { line = htmltemplate.HTMLEscapeString(line) } else { t := strings.Split(line[80:], " ") line = htmltemplate.HTMLEscapeString(line[:80]) for i, s := range t { if i > 0 { line += " " } if s == "" || !isRef(s) { line += htmltemplate.HTMLEscapeString(s) continue } file, linenumber, isCode := parseRef(s) target := "" if isCode { target = ` target="code"` } line += fmt.Sprintf(` %s:%d`, file, linenumber, target, file, linenumber) } } n := i + 1 _, err := fmt.Fprintf(&b, `
%d%s
%s`, n, n, n, line, "\n") xcheckf(err, "writing rfc line") } fmt.Fprint(&b, ` `) xwritefile(dst, b.Bytes()) } } // Generate overal file. index := indexHTML if release { index = strings.ReplaceAll(index, "RELEASEWEIGHT", "bold") index = strings.ReplaceAll(index, "REVISIONWEIGHT", "normal") } else { index = strings.ReplaceAll(index, "RELEASEWEIGHT", "normal") index = strings.ReplaceAll(index, "REVISIONWEIGHT", "bold") } index = strings.ReplaceAll(index, "REVISION", revision) index = strings.ReplaceAll(index, "DATE", date) index = strings.ReplaceAll(index, "RELEASE", latestRelease) xwritefile("index.html", []byte(index)) } var indexHTML = ` Cross-referenced code and RFCs - Mox
mox, cross-referenced code and RFCs: RELEASE dev (commit REVISION, DATE)
..., index
..., index
` var codeTemplate = htmltemplate.Must(htmltemplate.New("code").Parse(` code index {{- range $dir, $files := .Dirs }} {{- end }}
PackageFiles
{{ $dir }}/ {{- range $files }} {{ . }} {{- end }}
`)) var rfcTemplate = htmltemplate.Must(htmltemplate.New("rfc").Parse(` {{- range $topic, $rfcs := .Topics }} {{- end }}
TopicRFC
{{ $topic }} {{- range $rfcs }} {{ .File }} {{- end }}
`))