//go:build website package main import ( "bufio" "bytes" "errors" "flag" "fmt" "html" htmltemplate "html/template" "io" "log" "os" "slices" "strconv" "strings" "github.com/russross/blackfriday/v2" ) func xcheck(err error, msg string) { if err != nil { log.Fatalf("%s: %s", msg, err) } } func main() { var commithash = os.Getenv("commithash") var commitdate = os.Getenv("commitdate") var pageRoot, pageProtocols bool var pageTitle string flag.BoolVar(&pageRoot, "root", false, "is top-level index page, instead of in a sub directory") flag.BoolVar(&pageProtocols, "protocols", false, "is protocols page") flag.StringVar(&pageTitle, "title", "", "html title of page, set to value of link name with a suffix") flag.Parse() args := flag.Args() if len(args) != 1 { flag.Usage() os.Exit(2) } linkname := args[0] if pageTitle == "" && linkname != "" { pageTitle = linkname + " - Mox" } // Often the website markdown file. input, err := io.ReadAll(os.Stdin) xcheck(err, "read") // For rendering the main content of the page. r := &renderer{ linkname == "Config reference", "", *blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{HeadingIDPrefix: "hdr-"}), } opts := []blackfriday.Option{ blackfriday.WithExtensions(blackfriday.CommonExtensions | blackfriday.AutoHeadingIDs), blackfriday.WithRenderer(r), } // Make table of contents of a page, based on h2-links, or "## ..." in markdown. makeTOC := func() ([]byte, []byte) { var title string // Get the h2's, split them over the columns. type toclink struct { Title string ID string } var links []toclink node := blackfriday.New(opts...).Parse(input) if node == nil { return nil, nil } for c := node.FirstChild; c != nil; c = c.Next { if c.Type != blackfriday.Heading { continue } if c.Level == 1 { title = string(c.FirstChild.Literal) } else if c.Level == 2 { link := toclink{string(c.FirstChild.Literal), c.HeadingID} links = append(links, link) } } // We split links over 2 columns if we have quite a few, to keep the page somewhat compact. ncol := 1 if len(links) > 6 { ncol = 2 } n := len(links) / ncol rem := len(links) - ncol*n counts := make([]int, ncol) for i := 0; i < ncol; i++ { counts[i] = n if rem > i { counts[i]++ } } toc := `
` toc += "\n" o := 0 for _, n := range counts { toc += "\n" o += n } toc += "
\n" var titlebuf []byte if title != "" { titlebuf = []byte(fmt.Sprintf(`

%s

`, html.EscapeString("hdr-"+blackfriday.SanitizedAnchorName(title)), html.EscapeString(title))) } return titlebuf, []byte(toc) } var output []byte if pageRoot { // Split content into two parts for main page. First two lines are special, for // header. inputstr := string(input) lines := strings.SplitN(inputstr, "\n", 3) if len(lines) < 2 { log.Fatalf("missing header") } inputstr = inputstr[len(lines[0])+1+len(lines[1])+1:] lines[0] = strings.TrimPrefix(lines[0], "#") lines[1] = strings.TrimPrefix(lines[1], "##") sep := "## Quickstart demo" inleft, inright, found := strings.Cut(inputstr, sep) if !found { log.Fatalf("did not find separator %q", sep) } outleft := blackfriday.Run([]byte(inleft), opts...) outright := blackfriday.Run([]byte(sep+inright), opts...) output = []byte(fmt.Sprintf(`

%s

%s

%s
%s
`, html.EscapeString(lines[0]), html.EscapeString(lines[1]), outleft, outright)) } else if pageProtocols { // ../rfc/index.txt is the standard input. We'll read each topic and the RFCs. topics := parseTopics(input) // First part of content is in markdown file. summary, err := os.ReadFile("protocols/summary.md") xcheck(err, "reading protocol summary") output = blackfriday.Run(summary, opts...) var out bytes.Buffer _, err = out.Write(output) xcheck(err, "write") err = protocolTemplate.Execute(&out, map[string]any{"Topics": topics}) xcheck(err, "render protocol support") output = out.Bytes() } else { // Other pages. xinput := input if bytes.HasPrefix(xinput, []byte("# ")) { xinput = bytes.SplitN(xinput, []byte("\n"), 2)[1] } output = blackfriday.Run(xinput, opts...) titlebuf, toc := makeTOC() output = append(toc, output...) output = append(titlebuf, output...) } // HTML preamble. before = strings.Replace(before, "...", ""+html.EscapeString(pageTitle)+"", 1) before = strings.Replace(before, ">"+linkname+"<", ` style="font-weight: bold">`+linkname+"<", 1) if !pageRoot { before = strings.ReplaceAll(before, `"./`, `"../`) } _, err = os.Stdout.Write([]byte(before)) xcheck(err, "write") // Page content. _, err = os.Stdout.Write(output) xcheck(err, "write") // Bottom, HTML closing. after = strings.Replace(after, "[commit]", fmt.Sprintf("%s, commit %s", commitdate, commithash), 1) _, err = os.Stdout.Write([]byte(after)) xcheck(err, "write") } // Implementation status of standards/protocols. type Status string const ( Implemented Status = "Yes" Partial Status = "Partial" Roadmap Status = "Roadmap" NotImplemented Status = "No" Unknown Status = "?" ) // RFC and its implementation status. type RFC struct { Number int Title string Status Status StatusClass string Obsolete bool } // Topic is a group of RFC's, typically by protocol, e.g. SMTP. type Topic struct { Title string ID string RFCs []RFC } // parse topics and RFCs from ../rfc/index.txt. // headings are topics, and hold the RFCs that follow them. func parseTopics(input []byte) []Topic { var l []Topic var t *Topic b := bufio.NewReader(bytes.NewReader(input)) for { line, err := b.ReadString('\n') if line != "" { if strings.HasPrefix(line, "# ") { // Skip topics without RFCs to show on the website. if t != nil && len(t.RFCs) == 0 { l = l[:len(l)-1] } title := strings.TrimPrefix(line, "# ") id := blackfriday.SanitizedAnchorName(title) l = append(l, Topic{Title: title, ID: id}) t = &l[len(l)-1] // RFCs will be added to t. continue } // Tokens: RFC number, implementation status, is obsolete, title. tokens := strings.Split(line, "\t") if len(tokens) != 4 { continue } ignore := strings.HasPrefix(tokens[1], "-") if ignore { continue } status := Status(strings.TrimPrefix(tokens[1], "-")) var statusClass string switch status { case Implemented: statusClass = "implemented" case Partial: statusClass = "partial" case Roadmap: statusClass = "roadmap" case NotImplemented: statusClass = "notimplemented" case Unknown: statusClass = "unknown" default: log.Fatalf("unknown implementation status %q, line %q", status, line) } number, err := strconv.ParseInt(tokens[0], 10, 32) xcheck(err, "parsing rfc number") flags := strings.Split(tokens[2], ",") title := tokens[3] rfc := RFC{ int(number), title, status, statusClass, slices.Contains(flags, "Obs"), } t.RFCs = append(t.RFCs, rfc) } if err == io.EOF { break } xcheck(err, "read line") } // Skip topics without RFCs to show on the website. if t != nil && len(t.RFCs) == 0 { l = l[:len(l)-1] } return l } // renderer is used for all HTML pages, for showing links to h2's on hover, and for // specially rendering the config files with links for each config field. type renderer struct { codeBlockConfigFile bool // Whether to interpret codeblocks as config files. h2 string // Current title, for config line IDs. blackfriday.HTMLRenderer // Embedded for RenderFooter and RenderHeader. } func (r *renderer) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus { if node.Type == blackfriday.Heading && node.Level == 2 { r.h2 = string(node.FirstChild.Literal) id := "hdr-" + blackfriday.SanitizedAnchorName(string(node.FirstChild.Literal)) if entering { _, err := fmt.Fprintf(w, `

`, id) xcheck(err, "write") } else { _, err := fmt.Fprintf(w, ` #

`, id) xcheck(err, "write") } return blackfriday.GoToNext } if r.codeBlockConfigFile && node.Type == blackfriday.CodeBlock { if !entering { log.Fatalf("not entering") } _, err := fmt.Fprintln(w, `
`) xcheck(err, "write") r.writeConfig(w, node.Literal) _, err = fmt.Fprintln(w, "
") xcheck(err, "write") return blackfriday.GoToNext } return r.HTMLRenderer.RenderNode(w, node, entering) } func (r *renderer) writeConfig(w io.Writer, data []byte) { var fields []string for _, line := range bytes.Split(data, []byte("\n")) { var attrs, link string s := string(line) text := strings.TrimLeft(s, "\t") if strings.HasPrefix(text, "#") { attrs = ` class="comment"` } else if text != "" { // Add id attribute and link to it, based on the nested config fields that lead here. ntab := len(s) - len(text) nfields := ntab + 1 if len(fields) >= nfields { fields = fields[:nfields] } else if nfields > len(fields)+1 { xcheck(errors.New("indent jumped"), "write codeblock") } else { fields = append(fields, "") } var word string if text == "-" { word = "dash" } else { word = strings.Split(text, ":")[0] } fields[nfields-1] = word id := fmt.Sprintf("cfg-%s-%s", blackfriday.SanitizedAnchorName(r.h2), strings.Join(fields, "-")) attrs = fmt.Sprintf(` id="%s"`, id) link = fmt.Sprintf(` #`, id) } if s == "" { line = []byte("\n") // Prevent empty, zero-height line. } _, err := fmt.Fprintf(w, "%s%s\n", attrs, html.EscapeString(string(line)), link) xcheck(err, "write codeblock") } } var before = ` ... ` // Template for protocol page, minus the first section which is read from // protocols/summary.md. var protocolTemplate = htmltemplate.Must(htmltemplate.New("protocolsupport").Parse(`
Yes All/most of the functionality of the RFC has been implemented.
Partial Some of the functionality from the RFC has been implemented.
Roadmap Implementing functionality from the RFC is on the roadmap.
No Functionality from the RFC has not been implemented, is not currently on the roadmap, but may be in the future.
? Status undecided, unknown or not applicable.
{{ range .Topics }} {{ range .RFCs }} {{ end }} {{ end }}
RFC # Status Title
{{ .Title }} #
{{ .Number }} {{ .Status }} {{ if .Obsolete }}Obsolete: {{ end }}{{ .Title }}
`))