Home | History | Annotate | Download | only in util
      1 // doc generates HTML files from the comments in header files.
      2 //
      3 // doc expects to be given the path to a JSON file via the --config option.
      4 // From that JSON (which is defined by the Config struct) it reads a list of
      5 // header file locations and generates HTML files for each in the current
      6 // directory.
      7 
      8 package main
      9 
     10 import (
     11 	"bufio"
     12 	"encoding/json"
     13 	"errors"
     14 	"flag"
     15 	"fmt"
     16 	"html/template"
     17 	"io/ioutil"
     18 	"os"
     19 	"path/filepath"
     20 	"strings"
     21 )
     22 
     23 // Config describes the structure of the config JSON file.
     24 type Config struct {
     25 	// BaseDirectory is a path to which other paths in the file are
     26 	// relative.
     27 	BaseDirectory string
     28 	Sections      []ConfigSection
     29 }
     30 
     31 type ConfigSection struct {
     32 	Name string
     33 	// Headers is a list of paths to header files.
     34 	Headers []string
     35 }
     36 
     37 // HeaderFile is the internal representation of a header file.
     38 type HeaderFile struct {
     39 	// Name is the basename of the header file (e.g. "ex_data.html").
     40 	Name string
     41 	// Preamble contains a comment for the file as a whole. Each string
     42 	// is a separate paragraph.
     43 	Preamble []string
     44 	Sections []HeaderSection
     45 }
     46 
     47 type HeaderSection struct {
     48 	// Preamble contains a comment for a group of functions.
     49 	Preamble []string
     50 	Decls    []HeaderDecl
     51 	// Num is just the index of the section. It's included in order to help
     52 	// text/template generate anchors.
     53 	Num int
     54 	// IsPrivate is true if the section contains private functions (as
     55 	// indicated by its name).
     56 	IsPrivate bool
     57 }
     58 
     59 type HeaderDecl struct {
     60 	// Comment contains a comment for a specific function. Each string is a
     61 	// paragraph. Some paragraph may contain \n runes to indicate that they
     62 	// are preformatted.
     63 	Comment []string
     64 	// Name contains the name of the function, if it could be extracted.
     65 	Name string
     66 	// Decl contains the preformatted C declaration itself.
     67 	Decl string
     68 	// Num is an index for the declaration, but the value is unique for all
     69 	// declarations in a HeaderFile. It's included in order to help
     70 	// text/template generate anchors.
     71 	Num int
     72 }
     73 
     74 const (
     75 	cppGuard     = "#if defined(__cplusplus)"
     76 	commentStart = "/* "
     77 	commentEnd   = " */"
     78 )
     79 
     80 func extractComment(lines []string, lineNo int) (comment []string, rest []string, restLineNo int, err error) {
     81 	if len(lines) == 0 {
     82 		return nil, lines, lineNo, nil
     83 	}
     84 
     85 	restLineNo = lineNo
     86 	rest = lines
     87 
     88 	if !strings.HasPrefix(rest[0], commentStart) {
     89 		panic("extractComment called on non-comment")
     90 	}
     91 	commentParagraph := rest[0][len(commentStart):]
     92 	rest = rest[1:]
     93 	restLineNo++
     94 
     95 	for len(rest) > 0 {
     96 		i := strings.Index(commentParagraph, commentEnd)
     97 		if i >= 0 {
     98 			if i != len(commentParagraph)-len(commentEnd) {
     99 				err = fmt.Errorf("garbage after comment end on line %d", restLineNo)
    100 				return
    101 			}
    102 			commentParagraph = commentParagraph[:i]
    103 			if len(commentParagraph) > 0 {
    104 				comment = append(comment, commentParagraph)
    105 			}
    106 			return
    107 		}
    108 
    109 		line := rest[0]
    110 		if !strings.HasPrefix(line, " *") {
    111 			err = fmt.Errorf("comment doesn't start with block prefix on line %d: %s", restLineNo, line)
    112 			return
    113 		}
    114 		line = line[2:]
    115 		if strings.HasPrefix(line, "   ") {
    116 			/* Identing the lines of a paragraph marks them as
    117 			* preformatted. */
    118 			if len(commentParagraph) > 0 {
    119 				commentParagraph += "\n"
    120 			}
    121 			line = line[3:]
    122 		}
    123 		if len(line) > 0 {
    124 			commentParagraph = commentParagraph + line
    125 			if len(commentParagraph) > 0 && commentParagraph[0] == ' ' {
    126 				commentParagraph = commentParagraph[1:]
    127 			}
    128 		} else {
    129 			comment = append(comment, commentParagraph)
    130 			commentParagraph = ""
    131 		}
    132 		rest = rest[1:]
    133 		restLineNo++
    134 	}
    135 
    136 	err = errors.New("hit EOF in comment")
    137 	return
    138 }
    139 
    140 func extractDecl(lines []string, lineNo int) (decl string, rest []string, restLineNo int, err error) {
    141 	if len(lines) == 0 {
    142 		return "", lines, lineNo, nil
    143 	}
    144 
    145 	rest = lines
    146 	restLineNo = lineNo
    147 
    148 	var stack []rune
    149 	for len(rest) > 0 {
    150 		line := rest[0]
    151 		for _, c := range line {
    152 			switch c {
    153 			case '(', '{', '[':
    154 				stack = append(stack, c)
    155 			case ')', '}', ']':
    156 				if len(stack) == 0 {
    157 					err = fmt.Errorf("unexpected %c on line %d", c, restLineNo)
    158 					return
    159 				}
    160 				var expected rune
    161 				switch c {
    162 				case ')':
    163 					expected = '('
    164 				case '}':
    165 					expected = '{'
    166 				case ']':
    167 					expected = '['
    168 				default:
    169 					panic("internal error")
    170 				}
    171 				if last := stack[len(stack)-1]; last != expected {
    172 					err = fmt.Errorf("found %c when expecting %c on line %d", c, last, restLineNo)
    173 					return
    174 				}
    175 				stack = stack[:len(stack)-1]
    176 			}
    177 		}
    178 		if len(decl) > 0 {
    179 			decl += "\n"
    180 		}
    181 		decl += line
    182 		rest = rest[1:]
    183 		restLineNo++
    184 
    185 		if len(stack) == 0 && (len(decl) == 0 || decl[len(decl)-1] != '\\') {
    186 			break
    187 		}
    188 	}
    189 
    190 	return
    191 }
    192 
    193 func skipPast(s, skip string) string {
    194 	i := strings.Index(s, skip)
    195 	if i > 0 {
    196 		return s[len(skip):]
    197 	}
    198 	return s
    199 }
    200 
    201 func getNameFromDecl(decl string) (string, bool) {
    202 	if strings.HasPrefix(decl, "struct ") {
    203 		return "", false
    204 	}
    205 	decl = skipPast(decl, "STACK_OF(")
    206 	decl = skipPast(decl, "LHASH_OF(")
    207 	i := strings.Index(decl, "(")
    208 	if i < 0 {
    209 		return "", false
    210 	}
    211 	j := strings.LastIndex(decl[:i], " ")
    212 	if j < 0 {
    213 		return "", false
    214 	}
    215 	for j+1 < len(decl) && decl[j+1] == '*' {
    216 		j++
    217 	}
    218 	return decl[j+1 : i], true
    219 }
    220 
    221 func (config *Config) parseHeader(path string) (*HeaderFile, error) {
    222 	headerPath := filepath.Join(config.BaseDirectory, path)
    223 
    224 	headerFile, err := os.Open(headerPath)
    225 	if err != nil {
    226 		return nil, err
    227 	}
    228 	defer headerFile.Close()
    229 
    230 	scanner := bufio.NewScanner(headerFile)
    231 	var lines, oldLines []string
    232 	for scanner.Scan() {
    233 		lines = append(lines, scanner.Text())
    234 	}
    235 	if err := scanner.Err(); err != nil {
    236 		return nil, err
    237 	}
    238 
    239 	lineNo := 0
    240 	found := false
    241 	for i, line := range lines {
    242 		lineNo++
    243 		if line == cppGuard {
    244 			lines = lines[i+1:]
    245 			lineNo++
    246 			found = true
    247 			break
    248 		}
    249 	}
    250 
    251 	if !found {
    252 		return nil, errors.New("no C++ guard found")
    253 	}
    254 
    255 	if len(lines) == 0 || lines[0] != "extern \"C\" {" {
    256 		return nil, errors.New("no extern \"C\" found after C++ guard")
    257 	}
    258 	lineNo += 2
    259 	lines = lines[2:]
    260 
    261 	header := &HeaderFile{
    262 		Name: filepath.Base(path),
    263 	}
    264 
    265 	for i, line := range lines {
    266 		lineNo++
    267 		if len(line) > 0 {
    268 			lines = lines[i:]
    269 			break
    270 		}
    271 	}
    272 
    273 	oldLines = lines
    274 	if len(lines) > 0 && strings.HasPrefix(lines[0], commentStart) {
    275 		comment, rest, restLineNo, err := extractComment(lines, lineNo)
    276 		if err != nil {
    277 			return nil, err
    278 		}
    279 
    280 		if len(rest) > 0 && len(rest[0]) == 0 {
    281 			if len(rest) < 2 || len(rest[1]) != 0 {
    282 				return nil, errors.New("preamble comment should be followed by two blank lines")
    283 			}
    284 			header.Preamble = comment
    285 			lineNo = restLineNo + 2
    286 			lines = rest[2:]
    287 		} else {
    288 			lines = oldLines
    289 		}
    290 	}
    291 
    292 	var sectionNumber, declNumber int
    293 
    294 	for {
    295 		// Start of a section.
    296 		if len(lines) == 0 {
    297 			return nil, errors.New("unexpected end of file")
    298 		}
    299 		line := lines[0]
    300 		if line == cppGuard {
    301 			break
    302 		}
    303 
    304 		if len(line) == 0 {
    305 			return nil, fmt.Errorf("blank line at start of section on line %d", lineNo)
    306 		}
    307 
    308 		section := HeaderSection{
    309 			Num: sectionNumber,
    310 		}
    311 		sectionNumber++
    312 
    313 		if strings.HasPrefix(line, commentStart) {
    314 			comment, rest, restLineNo, err := extractComment(lines, lineNo)
    315 			if err != nil {
    316 				return nil, err
    317 			}
    318 			if len(rest) > 0 && len(rest[0]) == 0 {
    319 				section.Preamble = comment
    320 				section.IsPrivate = len(comment) > 0 && strings.HasPrefix(comment[0], "Private functions")
    321 				lines = rest[1:]
    322 				lineNo = restLineNo + 1
    323 			}
    324 		}
    325 
    326 		for len(lines) > 0 {
    327 			line := lines[0]
    328 			if len(line) == 0 {
    329 				lines = lines[1:]
    330 				lineNo++
    331 				break
    332 			}
    333 			if line == cppGuard {
    334 				return nil, errors.New("hit ending C++ guard while in section")
    335 			}
    336 
    337 			var comment []string
    338 			var decl string
    339 			if strings.HasPrefix(line, commentStart) {
    340 				comment, lines, lineNo, err = extractComment(lines, lineNo)
    341 				if err != nil {
    342 					return nil, err
    343 				}
    344 			}
    345 			if len(lines) == 0 {
    346 				return nil, errors.New("expected decl at EOF")
    347 			}
    348 			decl, lines, lineNo, err = extractDecl(lines, lineNo)
    349 			if err != nil {
    350 				return nil, err
    351 			}
    352 			name, ok := getNameFromDecl(decl)
    353 			if !ok {
    354 				name = ""
    355 			}
    356 			if last := len(section.Decls) - 1; len(name) == 0 && len(comment) == 0 && last >= 0 {
    357 				section.Decls[last].Decl += "\n" + decl
    358 			} else {
    359 				section.Decls = append(section.Decls, HeaderDecl{
    360 					Comment: comment,
    361 					Name:    name,
    362 					Decl:    decl,
    363 					Num:     declNumber,
    364 				})
    365 				declNumber++
    366 			}
    367 
    368 			if len(lines) > 0 && len(lines[0]) == 0 {
    369 				lines = lines[1:]
    370 				lineNo++
    371 			}
    372 		}
    373 
    374 		header.Sections = append(header.Sections, section)
    375 	}
    376 
    377 	return header, nil
    378 }
    379 
    380 func firstSentence(paragraphs []string) string {
    381 	if len(paragraphs) == 0 {
    382 		return ""
    383 	}
    384 	s := paragraphs[0]
    385 	i := strings.Index(s, ". ")
    386 	if i >= 0 {
    387 		return s[:i]
    388 	}
    389 	if lastIndex := len(s) - 1; s[lastIndex] == '.' {
    390 		return s[:lastIndex]
    391 	}
    392 	return s
    393 }
    394 
    395 func markupPipeWords(s string) template.HTML {
    396 	ret := ""
    397 
    398 	for {
    399 		i := strings.Index(s, "|")
    400 		if i == -1 {
    401 			ret += s
    402 			break
    403 		}
    404 		ret += s[:i]
    405 		s = s[i+1:]
    406 
    407 		i = strings.Index(s, "|")
    408 		j := strings.Index(s, " ")
    409 		if i > 0 && (j == -1 || j > i) {
    410 			ret += "<tt>"
    411 			ret += s[:i]
    412 			ret += "</tt>"
    413 			s = s[i+1:]
    414 		} else {
    415 			ret += "|"
    416 		}
    417 	}
    418 
    419 	return template.HTML(ret)
    420 }
    421 
    422 func markupFirstWord(s template.HTML) template.HTML {
    423 	i := strings.Index(string(s), " ")
    424 	if i > 0 {
    425 		return "<span class=\"first-word\">" + s[:i] + "</span>" + s[i:]
    426 	}
    427 	return s
    428 }
    429 
    430 func newlinesToBR(html template.HTML) template.HTML {
    431 	s := string(html)
    432 	if !strings.Contains(s, "\n") {
    433 		return html
    434 	}
    435 	s = strings.Replace(s, "\n", "<br>", -1)
    436 	s = strings.Replace(s, " ", "&nbsp;", -1)
    437 	return template.HTML(s)
    438 }
    439 
    440 func generate(outPath string, config *Config) (map[string]string, error) {
    441 	headerTmpl := template.New("headerTmpl")
    442 	headerTmpl.Funcs(template.FuncMap{
    443 		"firstSentence":   firstSentence,
    444 		"markupPipeWords": markupPipeWords,
    445 		"markupFirstWord": markupFirstWord,
    446 		"newlinesToBR":    newlinesToBR,
    447 	})
    448 	headerTmpl, err := headerTmpl.Parse(`<!DOCTYPE html5>
    449 <html>
    450   <head>
    451     <title>BoringSSL - {{.Name}}</title>
    452     <meta charset="utf-8">
    453     <link rel="stylesheet" type="text/css" href="doc.css">
    454   </head>
    455 
    456   <body>
    457     <div id="main">
    458     <h2>{{.Name}}</h2>
    459 
    460     {{range .Preamble}}<p>{{. | html | markupPipeWords}}</p>{{end}}
    461 
    462     <ol>
    463       {{range .Sections}}
    464         {{if not .IsPrivate}}
    465           {{if .Preamble}}<li class="header"><a href="#section-{{.Num}}">{{.Preamble | firstSentence}}</a></li>{{end}}
    466           {{range .Decls}}
    467             {{if .Name}}<li><a href="#decl-{{.Num}}"><tt>{{.Name}}</tt></a></li>{{end}}
    468           {{end}}
    469         {{end}}
    470       {{end}}
    471     </ol>
    472 
    473     {{range .Sections}}
    474       {{if not .IsPrivate}}
    475         <div class="section">
    476         {{if .Preamble}}
    477           <div class="sectionpreamble">
    478           <a name="section-{{.Num}}">
    479           {{range .Preamble}}<p>{{. | html | markupPipeWords}}</p>{{end}}
    480           </a>
    481           </div>
    482         {{end}}
    483 
    484         {{range .Decls}}
    485           <div class="decl">
    486           <a name="decl-{{.Num}}">
    487           {{range .Comment}}
    488             <p>{{. | html | markupPipeWords | newlinesToBR | markupFirstWord}}</p>
    489           {{end}}
    490           <pre>{{.Decl}}</pre>
    491           </a>
    492           </div>
    493         {{end}}
    494         </div>
    495       {{end}}
    496     {{end}}
    497     </div>
    498   </body>
    499 </html>`)
    500 	if err != nil {
    501 		return nil, err
    502 	}
    503 
    504 	headerDescriptions := make(map[string]string)
    505 
    506 	for _, section := range config.Sections {
    507 		for _, headerPath := range section.Headers {
    508 			header, err := config.parseHeader(headerPath)
    509 			if err != nil {
    510 				return nil, errors.New("while parsing " + headerPath + ": " + err.Error())
    511 			}
    512 			headerDescriptions[header.Name] = firstSentence(header.Preamble)
    513 			filename := filepath.Join(outPath, header.Name+".html")
    514 			file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
    515 			if err != nil {
    516 				panic(err)
    517 			}
    518 			defer file.Close()
    519 			if err := headerTmpl.Execute(file, header); err != nil {
    520 				return nil, err
    521 			}
    522 		}
    523 	}
    524 
    525 	return headerDescriptions, nil
    526 }
    527 
    528 func generateIndex(outPath string, config *Config, headerDescriptions map[string]string) error {
    529 	indexTmpl := template.New("indexTmpl")
    530 	indexTmpl.Funcs(template.FuncMap{
    531 		"baseName": filepath.Base,
    532 		"headerDescription": func(header string) string {
    533 			return headerDescriptions[header]
    534 		},
    535 	})
    536 	indexTmpl, err := indexTmpl.Parse(`<!DOCTYPE html5>
    537 
    538   <head>
    539     <title>BoringSSL - Headers</title>
    540     <meta charset="utf-8">
    541     <link rel="stylesheet" type="text/css" href="doc.css">
    542   </head>
    543 
    544   <body>
    545     <div id="main">
    546       <table>
    547         {{range .Sections}}
    548 	  <tr class="header"><td colspan="2">{{.Name}}</td></tr>
    549 	  {{range .Headers}}
    550 	    <tr><td><a href="{{. | baseName}}.html">{{. | baseName}}</a></td><td>{{. | baseName | headerDescription}}</td></tr>
    551 	  {{end}}
    552 	{{end}}
    553       </table>
    554     </div>
    555   </body>
    556 </html>`)
    557 
    558 	if err != nil {
    559 		return err
    560 	}
    561 
    562 	file, err := os.OpenFile(filepath.Join(outPath, "headers.html"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
    563 	if err != nil {
    564 		panic(err)
    565 	}
    566 	defer file.Close()
    567 
    568 	if err := indexTmpl.Execute(file, config); err != nil {
    569 		return err
    570 	}
    571 
    572 	return nil
    573 }
    574 
    575 func main() {
    576 	var (
    577 		configFlag *string = flag.String("config", "", "Location of config file")
    578 		outputDir  *string = flag.String("out", "", "Path to the directory where the output will be written")
    579 		config     Config
    580 	)
    581 
    582 	flag.Parse()
    583 
    584 	if len(*configFlag) == 0 {
    585 		fmt.Printf("No config file given by --config\n")
    586 		os.Exit(1)
    587 	}
    588 
    589 	if len(*outputDir) == 0 {
    590 		fmt.Printf("No output directory given by --out\n")
    591 		os.Exit(1)
    592 	}
    593 
    594 	configBytes, err := ioutil.ReadFile(*configFlag)
    595 	if err != nil {
    596 		fmt.Printf("Failed to open config file: %s\n", err)
    597 		os.Exit(1)
    598 	}
    599 
    600 	if err := json.Unmarshal(configBytes, &config); err != nil {
    601 		fmt.Printf("Failed to parse config file: %s\n", err)
    602 		os.Exit(1)
    603 	}
    604 
    605 	headerDescriptions, err := generate(*outputDir, &config)
    606 	if err != nil {
    607 		fmt.Printf("Failed to generate output: %s\n", err)
    608 		os.Exit(1)
    609 	}
    610 
    611 	if err := generateIndex(*outputDir, &config, headerDescriptions); err != nil {
    612 		fmt.Printf("Failed to generate index: %s\n", err)
    613 		os.Exit(1)
    614 	}
    615 }
    616