//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"
for _, link := range links[o : o+n] {
toc += fmt.Sprintf(`
\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, `
")
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.