95c29f3cd1
Initial fork from f2d678e6e89b6508147086610e985d4e8416e867 (1.0.2 beta). (This change contains substantial changes from the original and effectively starts a new history.)
616 lines
14 KiB
Go
616 lines
14 KiB
Go
// 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"
|
|
"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
|
|
}
|
|
|
|
type HeaderSection struct {
|
|
// Preamble contains a comment for a group of functions.
|
|
Preamble []string
|
|
Decls []HeaderDecl
|
|
// Num is just the index of the section. It's included in order to help
|
|
// text/template generate anchors.
|
|
Num int
|
|
// 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
|
|
// Num is an index for the declaration, but the value is unique for all
|
|
// declarations in a HeaderFile. It's included in order to help
|
|
// text/template generate anchors.
|
|
Num int
|
|
}
|
|
|
|
const (
|
|
cppGuard = "#if defined(__cplusplus)"
|
|
commentStart = "/* "
|
|
commentEnd = " */"
|
|
)
|
|
|
|
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
|
|
|
|
if !strings.HasPrefix(rest[0], commentStart) {
|
|
panic("extractComment called on non-comment")
|
|
}
|
|
commentParagraph := rest[0][len(commentStart):]
|
|
rest = rest[1:]
|
|
restLineNo++
|
|
|
|
for len(rest) > 0 {
|
|
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 !strings.HasPrefix(line, " *") {
|
|
err = fmt.Errorf("comment doesn't start with block prefix on line %d: %s", restLineNo, line)
|
|
return
|
|
}
|
|
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 {
|
|
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 skipPast(s, skip string) string {
|
|
i := strings.Index(s, skip)
|
|
if i > 0 {
|
|
return s[len(skip):]
|
|
}
|
|
return s
|
|
}
|
|
|
|
func getNameFromDecl(decl string) (string, bool) {
|
|
if strings.HasPrefix(decl, "struct ") {
|
|
return "", false
|
|
}
|
|
decl = skipPast(decl, "STACK_OF(")
|
|
decl = skipPast(decl, "LHASH_OF(")
|
|
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 (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 := 0
|
|
found := false
|
|
for i, line := range lines {
|
|
lineNo++
|
|
if line == cppGuard {
|
|
lines = lines[i+1:]
|
|
lineNo++
|
|
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),
|
|
}
|
|
|
|
for i, line := range lines {
|
|
lineNo++
|
|
if len(line) > 0 {
|
|
lines = lines[i:]
|
|
break
|
|
}
|
|
}
|
|
|
|
oldLines = lines
|
|
if len(lines) > 0 && strings.HasPrefix(lines[0], commentStart) {
|
|
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
|
|
}
|
|
}
|
|
|
|
var sectionNumber, declNumber int
|
|
|
|
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)
|
|
}
|
|
|
|
section := HeaderSection{
|
|
Num: sectionNumber,
|
|
}
|
|
sectionNumber++
|
|
|
|
if strings.HasPrefix(line, commentStart) {
|
|
comment, rest, restLineNo, err := extractComment(lines, lineNo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(rest) > 0 && len(rest[0]) == 0 {
|
|
section.Preamble = comment
|
|
section.IsPrivate = len(comment) > 0 && strings.HasPrefix(comment[0], "Private functions")
|
|
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 strings.HasPrefix(line, commentStart) {
|
|
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")
|
|
}
|
|
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 {
|
|
section.Decls = append(section.Decls, HeaderDecl{
|
|
Comment: comment,
|
|
Name: name,
|
|
Decl: decl,
|
|
Num: declNumber,
|
|
})
|
|
declNumber++
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func markupPipeWords(s string) template.HTML {
|
|
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>"
|
|
ret += s[:i]
|
|
ret += "</tt>"
|
|
s = s[i+1:]
|
|
} else {
|
|
ret += "|"
|
|
}
|
|
}
|
|
|
|
return template.HTML(ret)
|
|
}
|
|
|
|
func markupFirstWord(s template.HTML) template.HTML {
|
|
i := strings.Index(string(s), " ")
|
|
if i > 0 {
|
|
return "<span class=\"first-word\">" + s[:i] + "</span>" + s[i:]
|
|
}
|
|
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) {
|
|
headerTmpl := template.New("headerTmpl")
|
|
headerTmpl.Funcs(template.FuncMap{
|
|
"firstSentence": firstSentence,
|
|
"markupPipeWords": markupPipeWords,
|
|
"markupFirstWord": markupFirstWord,
|
|
"newlinesToBR": newlinesToBR,
|
|
})
|
|
headerTmpl, err := headerTmpl.Parse(`<!DOCTYPE html5>
|
|
<html>
|
|
<head>
|
|
<title>BoringSSL - {{.Name}}</title>
|
|
<meta charset="utf-8">
|
|
<link rel="stylesheet" type="text/css" href="doc.css">
|
|
</head>
|
|
|
|
<body>
|
|
<div id="main">
|
|
<h2>{{.Name}}</h2>
|
|
|
|
{{range .Preamble}}<p>{{. | html | markupPipeWords}}</p>{{end}}
|
|
|
|
<ol>
|
|
{{range .Sections}}
|
|
{{if not .IsPrivate}}
|
|
{{if .Preamble}}<li class="header"><a href="#section-{{.Num}}">{{.Preamble | firstSentence}}</a></li>{{end}}
|
|
{{range .Decls}}
|
|
{{if .Name}}<li><a href="#decl-{{.Num}}"><tt>{{.Name}}</tt></a></li>{{end}}
|
|
{{end}}
|
|
{{end}}
|
|
{{end}}
|
|
</ol>
|
|
|
|
{{range .Sections}}
|
|
{{if not .IsPrivate}}
|
|
<div class="section">
|
|
{{if .Preamble}}
|
|
<div class="sectionpreamble">
|
|
<a name="section-{{.Num}}">
|
|
{{range .Preamble}}<p>{{. | html | markupPipeWords}}</p>{{end}}
|
|
</a>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{range .Decls}}
|
|
<div class="decl">
|
|
<a name="decl-{{.Num}}">
|
|
{{range .Comment}}
|
|
<p>{{. | html | markupPipeWords | newlinesToBR | markupFirstWord}}</p>
|
|
{{end}}
|
|
<pre>{{.Decl}}</pre>
|
|
</a>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
{{end}}
|
|
</div>
|
|
</body>
|
|
</html>`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
headerDescriptions := make(map[string]string)
|
|
|
|
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)
|
|
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">
|
|
<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 main() {
|
|
var (
|
|
configFlag *string = flag.String("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)
|
|
}
|
|
}
|