1 // doc generates HTML files from the comments in header files.
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
23 // Config describes the structure of the config JSON file.
25 // BaseDirectory is a path to which other paths in the file are
28 Sections []ConfigSection
31 type ConfigSection struct {
33 // Headers is a list of paths to header files.
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").
41 // Preamble contains a comment for the file as a whole. Each string
42 // is a separate paragraph.
44 Sections []HeaderSection
47 type HeaderSection struct {
48 // Preamble contains a comment for a group of functions.
51 // Num is just the index of the section. It's included in order to help
52 // text/template generate anchors.
54 // IsPrivate is true if the section contains private functions (as
55 // indicated by its name).
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
64 // Name contains the name of the function, if it could be extracted.
66 // Decl contains the preformatted C declaration itself.
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.
75 cppGuard = "#if defined(__cplusplus)"
80 func extractComment(lines []string, lineNo int) (comment []string, rest []string, restLineNo int, err error) {
82 return nil, lines, lineNo, nil
88 if !strings.HasPrefix(rest[0], commentStart) {
89 panic("extractComment called on non-comment")
91 commentParagraph := rest[0][len(commentStart):]
96 i := strings.Index(commentParagraph, commentEnd)
98 if i != len(commentParagraph)-len(commentEnd) {
99 err = fmt.Errorf("garbage after comment end on line %d", restLineNo)
102 commentParagraph = commentParagraph[:i]
103 if len(commentParagraph) > 0 {
104 comment = append(comment, commentParagraph)
110 if !strings.HasPrefix(line, " *") {
111 err = fmt.Errorf("comment doesn't start with block prefix on line %d: %s", restLineNo, line)
115 if strings.HasPrefix(line, " ") {
116 /* Identing the lines of a paragraph marks them as
118 if len(commentParagraph) > 0 {
119 commentParagraph += "\n"
124 commentParagraph = commentParagraph + line
125 if len(commentParagraph) > 0 && commentParagraph[0] == ' ' {
126 commentParagraph = commentParagraph[1:]
129 comment = append(comment, commentParagraph)
130 commentParagraph = ""
136 err = errors.New("hit EOF in comment")
140 func extractDecl(lines []string, lineNo int) (decl string, rest []string, restLineNo int, err error) {
142 return "", lines, lineNo, nil
151 for _, c := range line {
154 stack = append(stack, c)
157 err = fmt.Errorf("unexpected %c on line %d", c, restLineNo)
169 panic("internal error")
171 if last := stack[len(stack)-1]; last != expected {
172 err = fmt.Errorf("found %c when expecting %c on line %d", c, last, restLineNo)
175 stack = stack[:len(stack)-1]
185 if len(stack) == 0 && (len(decl) == 0 || decl[len(decl)-1] != '\\') {
193 func skipPast(s, skip string) string {
194 i := strings.Index(s, skip)
201 func getNameFromDecl(decl string) (string, bool) {
202 if strings.HasPrefix(decl, "struct ") {
205 decl = skipPast(decl, "STACK_OF(")
206 decl = skipPast(decl, "LHASH_OF(")
207 i := strings.Index(decl, "(")
211 j := strings.LastIndex(decl[:i], " ")
215 for j+1 < len(decl) && decl[j+1] == '*' {
218 return decl[j+1 : i], true
221 func (config *Config) parseHeader(path string) (*HeaderFile, error) {
222 headerPath := filepath.Join(config.BaseDirectory, path)
224 headerFile, err := os.Open(headerPath)
228 defer headerFile.Close()
230 scanner := bufio.NewScanner(headerFile)
231 var lines, oldLines []string
233 lines = append(lines, scanner.Text())
235 if err := scanner.Err(); err != nil {
241 for i, line := range lines {
243 if line == cppGuard {
252 return nil, errors.New("no C++ guard found")
255 if len(lines) == 0 || lines[0] != "extern \"C\" {" {
256 return nil, errors.New("no extern \"C\" found after C++ guard")
261 header := &HeaderFile{
262 Name: filepath.Base(path),
265 for i, line := range lines {
274 if len(lines) > 0 && strings.HasPrefix(lines[0], commentStart) {
275 comment, rest, restLineNo, err := extractComment(lines, lineNo)
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")
284 header.Preamble = comment
285 lineNo = restLineNo + 2
292 var sectionNumber, declNumber int
295 // Start of a section.
297 return nil, errors.New("unexpected end of file")
300 if line == cppGuard {
305 return nil, fmt.Errorf("blank line at start of section on line %d", lineNo)
308 section := HeaderSection{
313 if strings.HasPrefix(line, commentStart) {
314 comment, rest, restLineNo, err := extractComment(lines, lineNo)
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")
322 lineNo = restLineNo + 1
333 if line == cppGuard {
334 return nil, errors.New("hit ending C++ guard while in section")
339 if strings.HasPrefix(line, commentStart) {
340 comment, lines, lineNo, err = extractComment(lines, lineNo)
346 return nil, errors.New("expected decl at EOF")
348 decl, lines, lineNo, err = extractDecl(lines, lineNo)
352 name, ok := getNameFromDecl(decl)
356 if last := len(section.Decls) - 1; len(name) == 0 && len(comment) == 0 && last >= 0 {
357 section.Decls[last].Decl += "\n" + decl
359 section.Decls = append(section.Decls, HeaderDecl{
368 if len(lines) > 0 && len(lines[0]) == 0 {
374 header.Sections = append(header.Sections, section)
380 func firstSentence(paragraphs []string) string {
381 if len(paragraphs) == 0 {
385 i := strings.Index(s, ". ")
389 if lastIndex := len(s) - 1; s[lastIndex] == '.' {
395 func markupPipeWords(s string) template.HTML {
399 i := strings.Index(s, "|")
407 i = strings.Index(s, "|")
408 j := strings.Index(s, " ")
409 if i > 0 && (j == -1 || j > i) {
419 return template.HTML(ret)
422 func markupFirstWord(s template.HTML) template.HTML {
423 i := strings.Index(string(s), " ")
425 return "<span class=\"first-word\">" + s[:i] + "</span>" + s[i:]
430 func newlinesToBR(html template.HTML) template.HTML {
432 if !strings.Contains(s, "\n") {
435 s = strings.Replace(s, "\n", "<br>", -1)
436 s = strings.Replace(s, " ", " ", -1)
437 return template.HTML(s)
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,
448 headerTmpl, err := headerTmpl.Parse(`<!DOCTYPE html5>
451 <title>BoringSSL - {{.Name}}</title>
452 <meta charset="utf-8">
453 <link rel="stylesheet" type="text/css" href="doc.css">
460 {{range .Preamble}}<p>{{. | html | markupPipeWords}}</p>{{end}}
464 {{if not .IsPrivate}}
465 {{if .Preamble}}<li class="header"><a href="#section-{{.Num}}">{{.Preamble | firstSentence}}</a></li>{{end}}
467 {{if .Name}}<li><a href="#decl-{{.Num}}"><tt>{{.Name}}</tt></a></li>{{end}}
474 {{if not .IsPrivate}}
475 <div class="section">
477 <div class="sectionpreamble">
478 <a name="section-{{.Num}}">
479 {{range .Preamble}}<p>{{. | html | markupPipeWords}}</p>{{end}}
486 <a name="decl-{{.Num}}">
488 <p>{{. | html | markupPipeWords | newlinesToBR | markupFirstWord}}</p>
504 headerDescriptions := make(map[string]string)
506 for _, section := range config.Sections {
507 for _, headerPath := range section.Headers {
508 header, err := config.parseHeader(headerPath)
510 return nil, errors.New("while parsing " + headerPath + ": " + err.Error())
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)
519 if err := headerTmpl.Execute(file, header); err != nil {
525 return headerDescriptions, nil
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]
536 indexTmpl, err := indexTmpl.Parse(`<!DOCTYPE html5>
539 <title>BoringSSL - Headers</title>
540 <meta charset="utf-8">
541 <link rel="stylesheet" type="text/css" href="doc.css">
548 <tr class="header"><td colspan="2">{{.Name}}</td></tr>
550 <tr><td><a href="{{. | baseName}}.html">{{. | baseName}}</a></td><td>{{. | baseName | headerDescription}}</td></tr>
562 file, err := os.OpenFile(filepath.Join(outPath, "headers.html"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
568 if err := indexTmpl.Execute(file, config); err != nil {
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")
584 if len(*configFlag) == 0 {
585 fmt.Printf("No config file given by --config\n")
589 if len(*outputDir) == 0 {
590 fmt.Printf("No output directory given by --out\n")
594 configBytes, err := ioutil.ReadFile(*configFlag)
596 fmt.Printf("Failed to open config file: %s\n", err)
600 if err := json.Unmarshal(configBytes, &config); err != nil {
601 fmt.Printf("Failed to parse config file: %s\n", err)
605 headerDescriptions, err := generate(*outputDir, &config)
607 fmt.Printf("Failed to generate output: %s\n", err)
611 if err := generateIndex(*outputDir, &config, headerDescriptions); err != nil {
612 fmt.Printf("Failed to generate index: %s\n", err)