// doc generates HTML files from the comments in header files. // // doc expects to be given the path to a JSON file via the --config option. // From that JSON (which is defined by the Config struct) it reads a list of // header file locations and generates HTML files for each in the current // directory. package main import ( "bufio" "encoding/json" "errors" "flag" "fmt" "html/template" "io/ioutil" "os" "path/filepath" "regexp" "strings" ) // Config describes the structure of the config JSON file. type Config struct { // BaseDirectory is a path to which other paths in the file are // relative. BaseDirectory string Sections []ConfigSection } type ConfigSection struct { Name string // Headers is a list of paths to header files. Headers []string } // HeaderFile is the internal representation of a header file. type HeaderFile struct { // Name is the basename of the header file (e.g. "ex_data.html"). Name string // Preamble contains a comment for the file as a whole. Each string // is a separate paragraph. Preamble []string Sections []HeaderSection // AllDecls maps all decls to their URL fragments. AllDecls map[string]string } type HeaderSection struct { // Preamble contains a comment for a group of functions. Preamble []string Decls []HeaderDecl // Anchor, if non-empty, is the URL fragment to use in anchor tags. Anchor string // IsPrivate is true if the section contains private functions (as // indicated by its name). IsPrivate bool } type HeaderDecl struct { // Comment contains a comment for a specific function. Each string is a // paragraph. Some paragraph may contain \n runes to indicate that they // are preformatted. Comment []string // Name contains the name of the function, if it could be extracted. Name string // Decl contains the preformatted C declaration itself. Decl string // Anchor, if non-empty, is the URL fragment to use in anchor tags. Anchor string } const ( cppGuard = "#if defined(__cplusplus)" commentStart = "/* " commentEnd = " */" lineComment = "// " ) func isComment(line string) bool { return strings.HasPrefix(line, commentStart) || strings.HasPrefix(line, lineComment) } func commentSubject(line string) string { if strings.HasPrefix(line, "A ") { line = line[len("A "):] } else if strings.HasPrefix(line, "An ") { line = line[len("An "):] } idx := strings.IndexAny(line, " ,") if idx < 0 { return line } return line[:idx] } func extractComment(lines []string, lineNo int) (comment []string, rest []string, restLineNo int, err error) { if len(lines) == 0 { return nil, lines, lineNo, nil } restLineNo = lineNo rest = lines var isBlock bool if strings.HasPrefix(rest[0], commentStart) { isBlock = true } else if !strings.HasPrefix(rest[0], lineComment) { panic("extractComment called on non-comment") } commentParagraph := rest[0][len(commentStart):] rest = rest[1:] restLineNo++ for len(rest) > 0 { if isBlock { i := strings.Index(commentParagraph, commentEnd) if i >= 0 { if i != len(commentParagraph)-len(commentEnd) { err = fmt.Errorf("garbage after comment end on line %d", restLineNo) return } commentParagraph = commentParagraph[:i] if len(commentParagraph) > 0 { comment = append(comment, commentParagraph) } return } } line := rest[0] if isBlock { if !strings.HasPrefix(line, " *") { err = fmt.Errorf("comment doesn't start with block prefix on line %d: %s", restLineNo, line) return } } else if !strings.HasPrefix(line, "//") { if len(commentParagraph) > 0 { comment = append(comment, commentParagraph) } return } if len(line) == 2 || !isBlock || line[2] != '/' { line = line[2:] } if strings.HasPrefix(line, " ") { /* Identing the lines of a paragraph marks them as * preformatted. */ if len(commentParagraph) > 0 { commentParagraph += "\n" } line = line[3:] } if len(line) > 0 { commentParagraph = commentParagraph + line if len(commentParagraph) > 0 && commentParagraph[0] == ' ' { commentParagraph = commentParagraph[1:] } } else { comment = append(comment, commentParagraph) commentParagraph = "" } rest = rest[1:] restLineNo++ } err = errors.New("hit EOF in comment") return } func extractDecl(lines []string, lineNo int) (decl string, rest []string, restLineNo int, err error) { if len(lines) == 0 || len(lines[0]) == 0 { return "", lines, lineNo, nil } rest = lines restLineNo = lineNo var stack []rune for len(rest) > 0 { line := rest[0] for _, c := range line { switch c { case '(', '{', '[': stack = append(stack, c) case ')', '}', ']': if len(stack) == 0 { err = fmt.Errorf("unexpected %c on line %d", c, restLineNo) return } var expected rune switch c { case ')': expected = '(' case '}': expected = '{' case ']': expected = '[' default: panic("internal error") } if last := stack[len(stack)-1]; last != expected { err = fmt.Errorf("found %c when expecting %c on line %d", c, last, restLineNo) return } stack = stack[:len(stack)-1] } } if len(decl) > 0 { decl += "\n" } decl += line rest = rest[1:] restLineNo++ if len(stack) == 0 && (len(decl) == 0 || decl[len(decl)-1] != '\\') { break } } return } func skipLine(s string) string { i := strings.Index(s, "\n") if i > 0 { return s[i:] } return "" } var stackOfRegexp = regexp.MustCompile(`STACK_OF\(([^)]*)\)`) var lhashOfRegexp = regexp.MustCompile(`LHASH_OF\(([^)]*)\)`) func getNameFromDecl(decl string) (string, bool) { for strings.HasPrefix(decl, "#if") || strings.HasPrefix(decl, "#elif") { decl = skipLine(decl) } if strings.HasPrefix(decl, "typedef ") { return "", false } for _, prefix := range []string{"struct ", "enum ", "#define "} { if !strings.HasPrefix(decl, prefix) { continue } decl = strings.TrimPrefix(decl, prefix) for len(decl) > 0 && decl[0] == ' ' { decl = decl[1:] } // struct and enum types can be the return type of a // function. if prefix[0] != '#' && strings.Index(decl, "{") == -1 { break } i := strings.IndexAny(decl, "( ") if i < 0 { return "", false } return decl[:i], true } decl = strings.TrimPrefix(decl, "OPENSSL_EXPORT ") decl = strings.TrimPrefix(decl, "const ") decl = stackOfRegexp.ReplaceAllString(decl, "STACK_OF_$1") decl = lhashOfRegexp.ReplaceAllString(decl, "LHASH_OF_$1") i := strings.Index(decl, "(") if i < 0 { return "", false } j := strings.LastIndex(decl[:i], " ") if j < 0 { return "", false } for j+1 < len(decl) && decl[j+1] == '*' { j++ } return decl[j+1 : i], true } func sanitizeAnchor(name string) string { return strings.Replace(name, " ", "-", -1) } func isPrivateSection(name string) bool { return strings.HasPrefix(name, "Private functions") || strings.HasPrefix(name, "Private structures") || strings.Contains(name, "(hidden)") } func (config *Config) parseHeader(path string) (*HeaderFile, error) { headerPath := filepath.Join(config.BaseDirectory, path) headerFile, err := os.Open(headerPath) if err != nil { return nil, err } defer headerFile.Close() scanner := bufio.NewScanner(headerFile) var lines, oldLines []string for scanner.Scan() { lines = append(lines, scanner.Text()) } if err := scanner.Err(); err != nil { return nil, err } lineNo := 1 found := false for i, line := range lines { if line == cppGuard { lines = lines[i+1:] lineNo += i + 1 found = true break } } if !found { return nil, errors.New("no C++ guard found") } if len(lines) == 0 || lines[0] != "extern \"C\" {" { return nil, errors.New("no extern \"C\" found after C++ guard") } lineNo += 2 lines = lines[2:] header := &HeaderFile{ Name: filepath.Base(path), AllDecls: make(map[string]string), } for i, line := range lines { if len(line) > 0 { lines = lines[i:] lineNo += i break } } oldLines = lines if len(lines) > 0 && isComment(lines[0]) { comment, rest, restLineNo, err := extractComment(lines, lineNo) if err != nil { return nil, err } if len(rest) > 0 && len(rest[0]) == 0 { if len(rest) < 2 || len(rest[1]) != 0 { return nil, errors.New("preamble comment should be followed by two blank lines") } header.Preamble = comment lineNo = restLineNo + 2 lines = rest[2:] } else { lines = oldLines } } allAnchors := make(map[string]struct{}) for { // Start of a section. if len(lines) == 0 { return nil, errors.New("unexpected end of file") } line := lines[0] if line == cppGuard { break } if len(line) == 0 { return nil, fmt.Errorf("blank line at start of section on line %d", lineNo) } var section HeaderSection if isComment(line) { comment, rest, restLineNo, err := extractComment(lines, lineNo) if err != nil { return nil, err } if len(rest) > 0 && len(rest[0]) == 0 { anchor := sanitizeAnchor(firstSentence(comment)) if len(anchor) > 0 { if _, ok := allAnchors[anchor]; ok { return nil, fmt.Errorf("duplicate anchor: %s", anchor) } allAnchors[anchor] = struct{}{} } section.Preamble = comment section.IsPrivate = len(comment) > 0 && isPrivateSection(comment[0]) section.Anchor = anchor lines = rest[1:] lineNo = restLineNo + 1 } } for len(lines) > 0 { line := lines[0] if len(line) == 0 { lines = lines[1:] lineNo++ break } if line == cppGuard { return nil, errors.New("hit ending C++ guard while in section") } var comment []string var decl string if isComment(line) { comment, lines, lineNo, err = extractComment(lines, lineNo) if err != nil { return nil, err } } if len(lines) == 0 { return nil, errors.New("expected decl at EOF") } declLineNo := lineNo decl, lines, lineNo, err = extractDecl(lines, lineNo) if err != nil { return nil, err } name, ok := getNameFromDecl(decl) if !ok { name = "" } if last := len(section.Decls) - 1; len(name) == 0 && len(comment) == 0 && last >= 0 { section.Decls[last].Decl += "\n" + decl } else { // As a matter of style, comments should start // with the name of the thing that they are // commenting on. We make an exception here for // collective comments, which are detected by // starting with “The” or “These”. if len(comment) > 0 && len(name) > 0 && !strings.HasPrefix(comment[0], "The ") && !strings.HasPrefix(comment[0], "These ") { subject := commentSubject(comment[0]) ok := subject == name if l := len(subject); l > 0 && subject[l-1] == '*' { // Groups of names, notably #defines, are often // denoted with a wildcard. ok = strings.HasPrefix(name, subject[:l-1]) } if !ok { return nil, fmt.Errorf("comment for %q doesn't seem to match line %s:%d\n", name, path, declLineNo) } } anchor := sanitizeAnchor(name) // TODO(davidben): Enforce uniqueness. This is // skipped because #ifdefs currently result in // duplicate table-of-contents entries. allAnchors[anchor] = struct{}{} header.AllDecls[name] = anchor section.Decls = append(section.Decls, HeaderDecl{ Comment: comment, Name: name, Decl: decl, Anchor: anchor, }) } if len(lines) > 0 && len(lines[0]) == 0 { lines = lines[1:] lineNo++ } } header.Sections = append(header.Sections, section) } return header, nil } func firstSentence(paragraphs []string) string { if len(paragraphs) == 0 { return "" } s := paragraphs[0] i := strings.Index(s, ". ") if i >= 0 { return s[:i] } if lastIndex := len(s) - 1; s[lastIndex] == '.' { return s[:lastIndex] } return s } // markupPipeWords converts |s| into an HTML string, safe to be included outside // a tag, while also marking up words surrounded by |. func markupPipeWords(allDecls map[string]string, s string) template.HTML { // It is safe to look for '|' in the HTML-escaped version of |s| // below. The escaped version cannot include '|' instead tags because // there are no tags by construction. s = template.HTMLEscapeString(s) ret := "" for { i := strings.Index(s, "|") if i == -1 { ret += s break } ret += s[:i] s = s[i+1:] i = strings.Index(s, "|") j := strings.Index(s, " ") if i > 0 && (j == -1 || j > i) { ret += "<tt>" anchor, isLink := allDecls[s[:i]] if isLink { ret += fmt.Sprintf("<a href=\"%s\">", template.HTMLEscapeString(anchor)) } ret += s[:i] if isLink { ret += "</a>" } ret += "</tt>" s = s[i+1:] } else { ret += "|" } } return template.HTML(ret) } func markupFirstWord(s template.HTML) template.HTML { start := 0 again: end := strings.Index(string(s[start:]), " ") if end > 0 { end += start w := strings.ToLower(string(s[start:end])) // The first word was already marked up as an HTML tag. Don't // mark it up further. if strings.ContainsRune(w, '<') { return s } if w == "a" || w == "an" { start = end + 1 goto again } return s[:start] + "<span class=\"first-word\">" + s[start:end] + "</span>" + s[end:] } return s } func newlinesToBR(html template.HTML) template.HTML { s := string(html) if !strings.Contains(s, "\n") { return html } s = strings.Replace(s, "\n", "<br>", -1) s = strings.Replace(s, " ", " ", -1) return template.HTML(s) } func generate(outPath string, config *Config) (map[string]string, error) { allDecls := make(map[string]string) headerTmpl := template.New("headerTmpl") headerTmpl.Funcs(template.FuncMap{ "firstSentence": firstSentence, "markupPipeWords": func(s string) template.HTML { return markupPipeWords(allDecls, s) }, "markupFirstWord": markupFirstWord, "newlinesToBR": newlinesToBR, }) headerTmpl, err := headerTmpl.Parse(`<!DOCTYPE html> <html> <head> <title>BoringSSL - {{.Name}}</title> <meta charset="utf-8"> <link rel="stylesheet" type="text/css" href="doc.css"> </head> <body> <div id="main"> <div class="title"> <h2>{{.Name}}</h2> <a href="headers.html">All headers</a> </div> {{range .Preamble}}<p>{{. | markupPipeWords}}</p>{{end}} <ol> {{range .Sections}} {{if not .IsPrivate}} {{if .Anchor}}<li class="header"><a href="#{{.Anchor}}">{{.Preamble | firstSentence | markupPipeWords}}</a></li>{{end}} {{range .Decls}} {{if .Anchor}}<li><a href="#{{.Anchor}}"><tt>{{.Name}}</tt></a></li>{{end}} {{end}} {{end}} {{end}} </ol> {{range .Sections}} {{if not .IsPrivate}} <div class="section" {{if .Anchor}}id="{{.Anchor}}"{{end}}> {{if .Preamble}} <div class="sectionpreamble"> {{range .Preamble}}<p>{{. | markupPipeWords}}</p>{{end}} </div> {{end}} {{range .Decls}} <div class="decl" {{if .Anchor}}id="{{.Anchor}}"{{end}}> {{range .Comment}} <p>{{. | markupPipeWords | newlinesToBR | markupFirstWord}}</p> {{end}} <pre>{{.Decl}}</pre> </div> {{end}} </div> {{end}} {{end}} </div> </body> </html>`) if err != nil { return nil, err } headerDescriptions := make(map[string]string) var headers []*HeaderFile for _, section := range config.Sections { for _, headerPath := range section.Headers { header, err := config.parseHeader(headerPath) if err != nil { return nil, errors.New("while parsing " + headerPath + ": " + err.Error()) } headerDescriptions[header.Name] = firstSentence(header.Preamble) headers = append(headers, header) for name, anchor := range header.AllDecls { allDecls[name] = fmt.Sprintf("%s#%s", header.Name+".html", anchor) } } } for _, header := range headers { filename := filepath.Join(outPath, header.Name+".html") file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) if err != nil { panic(err) } defer file.Close() if err := headerTmpl.Execute(file, header); err != nil { return nil, err } } return headerDescriptions, nil } func generateIndex(outPath string, config *Config, headerDescriptions map[string]string) error { indexTmpl := template.New("indexTmpl") indexTmpl.Funcs(template.FuncMap{ "baseName": filepath.Base, "headerDescription": func(header string) string { return headerDescriptions[header] }, }) indexTmpl, err := indexTmpl.Parse(`<!DOCTYPE html5> <head> <title>BoringSSL - Headers</title> <meta charset="utf-8"> <link rel="stylesheet" type="text/css" href="doc.css"> </head> <body> <div id="main"> <div class="title"> <h2>BoringSSL Headers</h2> </div> <table> {{range .Sections}} <tr class="header"><td colspan="2">{{.Name}}</td></tr> {{range .Headers}} <tr><td><a href="{{. | baseName}}.html">{{. | baseName}}</a></td><td>{{. | baseName | headerDescription}}</td></tr> {{end}} {{end}} </table> </div> </body> </html>`) if err != nil { return err } file, err := os.OpenFile(filepath.Join(outPath, "headers.html"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) if err != nil { panic(err) } defer file.Close() if err := indexTmpl.Execute(file, config); err != nil { return err } return nil } func copyFile(outPath string, inFilePath string) error { bytes, err := ioutil.ReadFile(inFilePath) if err != nil { return err } return ioutil.WriteFile(filepath.Join(outPath, filepath.Base(inFilePath)), bytes, 0666) } func main() { var ( configFlag *string = flag.String("config", "doc.config", "Location of config file") outputDir *string = flag.String("out", ".", "Path to the directory where the output will be written") config Config ) flag.Parse() if len(*configFlag) == 0 { fmt.Printf("No config file given by --config\n") os.Exit(1) } if len(*outputDir) == 0 { fmt.Printf("No output directory given by --out\n") os.Exit(1) } configBytes, err := ioutil.ReadFile(*configFlag) if err != nil { fmt.Printf("Failed to open config file: %s\n", err) os.Exit(1) } if err := json.Unmarshal(configBytes, &config); err != nil { fmt.Printf("Failed to parse config file: %s\n", err) os.Exit(1) } headerDescriptions, err := generate(*outputDir, &config) if err != nil { fmt.Printf("Failed to generate output: %s\n", err) os.Exit(1) } if err := generateIndex(*outputDir, &config, headerDescriptions); err != nil { fmt.Printf("Failed to generate index: %s\n", err) os.Exit(1) } if err := copyFile(*outputDir, "doc.css"); err != nil { fmt.Printf("Failed to copy static file: %s\n", err) os.Exit(1) } }