blob: 9d6d57690325fbca75f81aec3461decb12f19bfb [file] [log] [blame]
Adam Langley95c29f32014-06-20 12:00:00 -07001// 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
8package main
9
10import (
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.
24type 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
31type 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.
38type 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
47type HeaderSection struct {
48 // Preamble contains a comment for a group of functions.
49 Preamble []string
50 Decls []HeaderDecl
David Benjamin1bfce802015-09-07 13:21:08 -040051 // Anchor, if non-empty, is the URL fragment to use in anchor tags.
52 Anchor string
Adam Langley95c29f32014-06-20 12:00:00 -070053 // IsPrivate is true if the section contains private functions (as
54 // indicated by its name).
55 IsPrivate bool
56}
57
58type HeaderDecl struct {
59 // Comment contains a comment for a specific function. Each string is a
60 // paragraph. Some paragraph may contain \n runes to indicate that they
61 // are preformatted.
62 Comment []string
63 // Name contains the name of the function, if it could be extracted.
64 Name string
65 // Decl contains the preformatted C declaration itself.
66 Decl string
David Benjamin1bfce802015-09-07 13:21:08 -040067 // Anchor, if non-empty, is the URL fragment to use in anchor tags.
68 Anchor string
Adam Langley95c29f32014-06-20 12:00:00 -070069}
70
71const (
72 cppGuard = "#if defined(__cplusplus)"
73 commentStart = "/* "
74 commentEnd = " */"
75)
76
77func extractComment(lines []string, lineNo int) (comment []string, rest []string, restLineNo int, err error) {
78 if len(lines) == 0 {
79 return nil, lines, lineNo, nil
80 }
81
82 restLineNo = lineNo
83 rest = lines
84
85 if !strings.HasPrefix(rest[0], commentStart) {
86 panic("extractComment called on non-comment")
87 }
88 commentParagraph := rest[0][len(commentStart):]
89 rest = rest[1:]
90 restLineNo++
91
92 for len(rest) > 0 {
93 i := strings.Index(commentParagraph, commentEnd)
94 if i >= 0 {
95 if i != len(commentParagraph)-len(commentEnd) {
96 err = fmt.Errorf("garbage after comment end on line %d", restLineNo)
97 return
98 }
99 commentParagraph = commentParagraph[:i]
100 if len(commentParagraph) > 0 {
101 comment = append(comment, commentParagraph)
102 }
103 return
104 }
105
106 line := rest[0]
107 if !strings.HasPrefix(line, " *") {
108 err = fmt.Errorf("comment doesn't start with block prefix on line %d: %s", restLineNo, line)
109 return
110 }
David Benjamin48b31502015-04-08 23:17:55 -0400111 if len(line) == 2 || line[2] != '/' {
112 line = line[2:]
113 }
Adam Langley95c29f32014-06-20 12:00:00 -0700114 if strings.HasPrefix(line, " ") {
115 /* Identing the lines of a paragraph marks them as
116 * preformatted. */
117 if len(commentParagraph) > 0 {
118 commentParagraph += "\n"
119 }
120 line = line[3:]
121 }
122 if len(line) > 0 {
123 commentParagraph = commentParagraph + line
124 if len(commentParagraph) > 0 && commentParagraph[0] == ' ' {
125 commentParagraph = commentParagraph[1:]
126 }
127 } else {
128 comment = append(comment, commentParagraph)
129 commentParagraph = ""
130 }
131 rest = rest[1:]
132 restLineNo++
133 }
134
135 err = errors.New("hit EOF in comment")
136 return
137}
138
139func extractDecl(lines []string, lineNo int) (decl string, rest []string, restLineNo int, err error) {
140 if len(lines) == 0 {
141 return "", lines, lineNo, nil
142 }
143
144 rest = lines
145 restLineNo = lineNo
146
147 var stack []rune
148 for len(rest) > 0 {
149 line := rest[0]
150 for _, c := range line {
151 switch c {
152 case '(', '{', '[':
153 stack = append(stack, c)
154 case ')', '}', ']':
155 if len(stack) == 0 {
156 err = fmt.Errorf("unexpected %c on line %d", c, restLineNo)
157 return
158 }
159 var expected rune
160 switch c {
161 case ')':
162 expected = '('
163 case '}':
164 expected = '{'
165 case ']':
166 expected = '['
167 default:
168 panic("internal error")
169 }
170 if last := stack[len(stack)-1]; last != expected {
171 err = fmt.Errorf("found %c when expecting %c on line %d", c, last, restLineNo)
172 return
173 }
174 stack = stack[:len(stack)-1]
175 }
176 }
177 if len(decl) > 0 {
178 decl += "\n"
179 }
180 decl += line
181 rest = rest[1:]
182 restLineNo++
183
184 if len(stack) == 0 && (len(decl) == 0 || decl[len(decl)-1] != '\\') {
185 break
186 }
187 }
188
189 return
190}
191
192func skipPast(s, skip string) string {
193 i := strings.Index(s, skip)
194 if i > 0 {
David Benjamin48b31502015-04-08 23:17:55 -0400195 return s[i:]
Adam Langley95c29f32014-06-20 12:00:00 -0700196 }
197 return s
198}
199
David Benjamin71485af2015-04-09 00:06:03 -0400200func skipLine(s string) string {
201 i := strings.Index(s, "\n")
202 if i > 0 {
203 return s[i:]
204 }
205 return ""
206}
207
Adam Langley95c29f32014-06-20 12:00:00 -0700208func getNameFromDecl(decl string) (string, bool) {
David Benjaminb4804282015-05-16 12:12:31 -0400209 for strings.HasPrefix(decl, "#if") || strings.HasPrefix(decl, "#elif") {
David Benjamin71485af2015-04-09 00:06:03 -0400210 decl = skipLine(decl)
211 }
Adam Langley95c29f32014-06-20 12:00:00 -0700212 if strings.HasPrefix(decl, "struct ") {
213 return "", false
214 }
David Benjamin6deacb32015-05-16 12:00:51 -0400215 if strings.HasPrefix(decl, "#define ") {
216 // This is a preprocessor #define. The name is the next symbol.
217 decl = strings.TrimPrefix(decl, "#define ")
218 for len(decl) > 0 && decl[0] == ' ' {
219 decl = decl[1:]
220 }
221 i := strings.IndexAny(decl, "( ")
222 if i < 0 {
223 return "", false
224 }
225 return decl[:i], true
226 }
Adam Langley95c29f32014-06-20 12:00:00 -0700227 decl = skipPast(decl, "STACK_OF(")
228 decl = skipPast(decl, "LHASH_OF(")
229 i := strings.Index(decl, "(")
230 if i < 0 {
231 return "", false
232 }
233 j := strings.LastIndex(decl[:i], " ")
234 if j < 0 {
235 return "", false
236 }
237 for j+1 < len(decl) && decl[j+1] == '*' {
238 j++
239 }
240 return decl[j+1 : i], true
241}
242
David Benjamin1bfce802015-09-07 13:21:08 -0400243func sanitizeAnchor(name string) string {
244 return strings.Replace(name, " ", "-", -1)
245}
246
Adam Langley95c29f32014-06-20 12:00:00 -0700247func (config *Config) parseHeader(path string) (*HeaderFile, error) {
248 headerPath := filepath.Join(config.BaseDirectory, path)
249
250 headerFile, err := os.Open(headerPath)
251 if err != nil {
252 return nil, err
253 }
254 defer headerFile.Close()
255
256 scanner := bufio.NewScanner(headerFile)
257 var lines, oldLines []string
258 for scanner.Scan() {
259 lines = append(lines, scanner.Text())
260 }
261 if err := scanner.Err(); err != nil {
262 return nil, err
263 }
264
265 lineNo := 0
266 found := false
267 for i, line := range lines {
268 lineNo++
269 if line == cppGuard {
270 lines = lines[i+1:]
271 lineNo++
272 found = true
273 break
274 }
275 }
276
277 if !found {
278 return nil, errors.New("no C++ guard found")
279 }
280
281 if len(lines) == 0 || lines[0] != "extern \"C\" {" {
282 return nil, errors.New("no extern \"C\" found after C++ guard")
283 }
284 lineNo += 2
285 lines = lines[2:]
286
287 header := &HeaderFile{
288 Name: filepath.Base(path),
289 }
290
291 for i, line := range lines {
292 lineNo++
293 if len(line) > 0 {
294 lines = lines[i:]
295 break
296 }
297 }
298
299 oldLines = lines
300 if len(lines) > 0 && strings.HasPrefix(lines[0], commentStart) {
301 comment, rest, restLineNo, err := extractComment(lines, lineNo)
302 if err != nil {
303 return nil, err
304 }
305
306 if len(rest) > 0 && len(rest[0]) == 0 {
307 if len(rest) < 2 || len(rest[1]) != 0 {
308 return nil, errors.New("preamble comment should be followed by two blank lines")
309 }
310 header.Preamble = comment
311 lineNo = restLineNo + 2
312 lines = rest[2:]
313 } else {
314 lines = oldLines
315 }
316 }
317
David Benjamin1bfce802015-09-07 13:21:08 -0400318 allAnchors := make(map[string]struct{})
Adam Langley95c29f32014-06-20 12:00:00 -0700319
320 for {
321 // Start of a section.
322 if len(lines) == 0 {
323 return nil, errors.New("unexpected end of file")
324 }
325 line := lines[0]
326 if line == cppGuard {
327 break
328 }
329
330 if len(line) == 0 {
331 return nil, fmt.Errorf("blank line at start of section on line %d", lineNo)
332 }
333
David Benjamin1bfce802015-09-07 13:21:08 -0400334 var section HeaderSection
Adam Langley95c29f32014-06-20 12:00:00 -0700335
336 if strings.HasPrefix(line, commentStart) {
337 comment, rest, restLineNo, err := extractComment(lines, lineNo)
338 if err != nil {
339 return nil, err
340 }
341 if len(rest) > 0 && len(rest[0]) == 0 {
David Benjamin1bfce802015-09-07 13:21:08 -0400342 anchor := sanitizeAnchor(firstSentence(comment))
343 if len(anchor) > 0 {
344 if _, ok := allAnchors[anchor]; ok {
345 return nil, fmt.Errorf("duplicate anchor: %s", anchor)
346 }
347 allAnchors[anchor] = struct{}{}
348 }
349
Adam Langley95c29f32014-06-20 12:00:00 -0700350 section.Preamble = comment
351 section.IsPrivate = len(comment) > 0 && strings.HasPrefix(comment[0], "Private functions")
David Benjamin1bfce802015-09-07 13:21:08 -0400352 section.Anchor = anchor
Adam Langley95c29f32014-06-20 12:00:00 -0700353 lines = rest[1:]
354 lineNo = restLineNo + 1
355 }
356 }
357
358 for len(lines) > 0 {
359 line := lines[0]
360 if len(line) == 0 {
361 lines = lines[1:]
362 lineNo++
363 break
364 }
365 if line == cppGuard {
366 return nil, errors.New("hit ending C++ guard while in section")
367 }
368
369 var comment []string
370 var decl string
371 if strings.HasPrefix(line, commentStart) {
372 comment, lines, lineNo, err = extractComment(lines, lineNo)
373 if err != nil {
374 return nil, err
375 }
376 }
377 if len(lines) == 0 {
378 return nil, errors.New("expected decl at EOF")
379 }
380 decl, lines, lineNo, err = extractDecl(lines, lineNo)
381 if err != nil {
382 return nil, err
383 }
384 name, ok := getNameFromDecl(decl)
385 if !ok {
386 name = ""
387 }
388 if last := len(section.Decls) - 1; len(name) == 0 && len(comment) == 0 && last >= 0 {
389 section.Decls[last].Decl += "\n" + decl
390 } else {
David Benjamin1bfce802015-09-07 13:21:08 -0400391 anchor := sanitizeAnchor(name)
392 // TODO(davidben): Enforce uniqueness. This is
393 // skipped because #ifdefs currently result in
394 // duplicate table-of-contents entries.
395 allAnchors[anchor] = struct{}{}
396
Adam Langley95c29f32014-06-20 12:00:00 -0700397 section.Decls = append(section.Decls, HeaderDecl{
398 Comment: comment,
399 Name: name,
400 Decl: decl,
David Benjamin1bfce802015-09-07 13:21:08 -0400401 Anchor: anchor,
Adam Langley95c29f32014-06-20 12:00:00 -0700402 })
Adam Langley95c29f32014-06-20 12:00:00 -0700403 }
404
405 if len(lines) > 0 && len(lines[0]) == 0 {
406 lines = lines[1:]
407 lineNo++
408 }
409 }
410
411 header.Sections = append(header.Sections, section)
412 }
413
414 return header, nil
415}
416
417func firstSentence(paragraphs []string) string {
418 if len(paragraphs) == 0 {
419 return ""
420 }
421 s := paragraphs[0]
422 i := strings.Index(s, ". ")
423 if i >= 0 {
424 return s[:i]
425 }
426 if lastIndex := len(s) - 1; s[lastIndex] == '.' {
427 return s[:lastIndex]
428 }
429 return s
430}
431
432func markupPipeWords(s string) template.HTML {
433 ret := ""
434
435 for {
436 i := strings.Index(s, "|")
437 if i == -1 {
438 ret += s
439 break
440 }
441 ret += s[:i]
442 s = s[i+1:]
443
444 i = strings.Index(s, "|")
445 j := strings.Index(s, " ")
446 if i > 0 && (j == -1 || j > i) {
447 ret += "<tt>"
448 ret += s[:i]
449 ret += "</tt>"
450 s = s[i+1:]
451 } else {
452 ret += "|"
453 }
454 }
455
456 return template.HTML(ret)
457}
458
459func markupFirstWord(s template.HTML) template.HTML {
David Benjamin5b082e82014-12-26 00:54:52 -0500460 start := 0
461again:
462 end := strings.Index(string(s[start:]), " ")
463 if end > 0 {
464 end += start
465 w := strings.ToLower(string(s[start:end]))
David Benjamin7e40d4e2015-09-07 13:17:45 -0400466 if w == "a" || w == "an" {
David Benjamin5b082e82014-12-26 00:54:52 -0500467 start = end + 1
468 goto again
469 }
470 return s[:start] + "<span class=\"first-word\">" + s[start:end] + "</span>" + s[end:]
Adam Langley95c29f32014-06-20 12:00:00 -0700471 }
472 return s
473}
474
475func newlinesToBR(html template.HTML) template.HTML {
476 s := string(html)
477 if !strings.Contains(s, "\n") {
478 return html
479 }
480 s = strings.Replace(s, "\n", "<br>", -1)
481 s = strings.Replace(s, " ", "&nbsp;", -1)
482 return template.HTML(s)
483}
484
485func generate(outPath string, config *Config) (map[string]string, error) {
486 headerTmpl := template.New("headerTmpl")
487 headerTmpl.Funcs(template.FuncMap{
488 "firstSentence": firstSentence,
489 "markupPipeWords": markupPipeWords,
490 "markupFirstWord": markupFirstWord,
491 "newlinesToBR": newlinesToBR,
492 })
David Benjamin5b082e82014-12-26 00:54:52 -0500493 headerTmpl, err := headerTmpl.Parse(`<!DOCTYPE html>
Adam Langley95c29f32014-06-20 12:00:00 -0700494<html>
495 <head>
496 <title>BoringSSL - {{.Name}}</title>
497 <meta charset="utf-8">
498 <link rel="stylesheet" type="text/css" href="doc.css">
499 </head>
500
501 <body>
502 <div id="main">
503 <h2>{{.Name}}</h2>
504
505 {{range .Preamble}}<p>{{. | html | markupPipeWords}}</p>{{end}}
506
507 <ol>
508 {{range .Sections}}
509 {{if not .IsPrivate}}
David Benjamin1bfce802015-09-07 13:21:08 -0400510 {{if .Anchor}}<li class="header"><a href="#{{.Anchor}}">{{.Preamble | firstSentence | html | markupPipeWords}}</a></li>{{end}}
Adam Langley95c29f32014-06-20 12:00:00 -0700511 {{range .Decls}}
David Benjamin1bfce802015-09-07 13:21:08 -0400512 {{if .Anchor}}<li><a href="#{{.Anchor}}"><tt>{{.Name}}</tt></a></li>{{end}}
Adam Langley95c29f32014-06-20 12:00:00 -0700513 {{end}}
514 {{end}}
515 {{end}}
516 </ol>
517
518 {{range .Sections}}
519 {{if not .IsPrivate}}
520 <div class="section">
521 {{if .Preamble}}
522 <div class="sectionpreamble">
David Benjamin1bfce802015-09-07 13:21:08 -0400523 <a{{if .Anchor}} name="{{.Anchor}}"{{end}}>
Adam Langley95c29f32014-06-20 12:00:00 -0700524 {{range .Preamble}}<p>{{. | html | markupPipeWords}}</p>{{end}}
525 </a>
526 </div>
527 {{end}}
528
529 {{range .Decls}}
530 <div class="decl">
David Benjamin1bfce802015-09-07 13:21:08 -0400531 <a{{if .Anchor}} name="{{.Anchor}}"{{end}}>
Adam Langley95c29f32014-06-20 12:00:00 -0700532 {{range .Comment}}
533 <p>{{. | html | markupPipeWords | newlinesToBR | markupFirstWord}}</p>
534 {{end}}
535 <pre>{{.Decl}}</pre>
536 </a>
537 </div>
538 {{end}}
539 </div>
540 {{end}}
541 {{end}}
542 </div>
543 </body>
544</html>`)
545 if err != nil {
546 return nil, err
547 }
548
549 headerDescriptions := make(map[string]string)
550
551 for _, section := range config.Sections {
552 for _, headerPath := range section.Headers {
553 header, err := config.parseHeader(headerPath)
554 if err != nil {
555 return nil, errors.New("while parsing " + headerPath + ": " + err.Error())
556 }
557 headerDescriptions[header.Name] = firstSentence(header.Preamble)
558 filename := filepath.Join(outPath, header.Name+".html")
559 file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
560 if err != nil {
561 panic(err)
562 }
563 defer file.Close()
564 if err := headerTmpl.Execute(file, header); err != nil {
565 return nil, err
566 }
567 }
568 }
569
570 return headerDescriptions, nil
571}
572
573func generateIndex(outPath string, config *Config, headerDescriptions map[string]string) error {
574 indexTmpl := template.New("indexTmpl")
575 indexTmpl.Funcs(template.FuncMap{
576 "baseName": filepath.Base,
577 "headerDescription": func(header string) string {
578 return headerDescriptions[header]
579 },
580 })
581 indexTmpl, err := indexTmpl.Parse(`<!DOCTYPE html5>
582
583 <head>
584 <title>BoringSSL - Headers</title>
585 <meta charset="utf-8">
586 <link rel="stylesheet" type="text/css" href="doc.css">
587 </head>
588
589 <body>
590 <div id="main">
591 <table>
592 {{range .Sections}}
593 <tr class="header"><td colspan="2">{{.Name}}</td></tr>
594 {{range .Headers}}
595 <tr><td><a href="{{. | baseName}}.html">{{. | baseName}}</a></td><td>{{. | baseName | headerDescription}}</td></tr>
596 {{end}}
597 {{end}}
598 </table>
599 </div>
600 </body>
601</html>`)
602
603 if err != nil {
604 return err
605 }
606
607 file, err := os.OpenFile(filepath.Join(outPath, "headers.html"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
608 if err != nil {
609 panic(err)
610 }
611 defer file.Close()
612
613 if err := indexTmpl.Execute(file, config); err != nil {
614 return err
615 }
616
617 return nil
618}
619
Brian Smith55a3cf42015-08-09 17:08:49 -0400620func copyFile(outPath string, inFilePath string) error {
621 bytes, err := ioutil.ReadFile(inFilePath)
622 if err != nil {
623 return err
624 }
625 return ioutil.WriteFile(filepath.Join(outPath, filepath.Base(inFilePath)), bytes, 0666)
626}
627
Adam Langley95c29f32014-06-20 12:00:00 -0700628func main() {
629 var (
Adam Langley0fd56392015-04-08 17:32:55 -0700630 configFlag *string = flag.String("config", "doc.config", "Location of config file")
631 outputDir *string = flag.String("out", ".", "Path to the directory where the output will be written")
Adam Langley95c29f32014-06-20 12:00:00 -0700632 config Config
633 )
634
635 flag.Parse()
636
637 if len(*configFlag) == 0 {
638 fmt.Printf("No config file given by --config\n")
639 os.Exit(1)
640 }
641
642 if len(*outputDir) == 0 {
643 fmt.Printf("No output directory given by --out\n")
644 os.Exit(1)
645 }
646
647 configBytes, err := ioutil.ReadFile(*configFlag)
648 if err != nil {
649 fmt.Printf("Failed to open config file: %s\n", err)
650 os.Exit(1)
651 }
652
653 if err := json.Unmarshal(configBytes, &config); err != nil {
654 fmt.Printf("Failed to parse config file: %s\n", err)
655 os.Exit(1)
656 }
657
658 headerDescriptions, err := generate(*outputDir, &config)
659 if err != nil {
660 fmt.Printf("Failed to generate output: %s\n", err)
661 os.Exit(1)
662 }
663
664 if err := generateIndex(*outputDir, &config, headerDescriptions); err != nil {
665 fmt.Printf("Failed to generate index: %s\n", err)
666 os.Exit(1)
667 }
Brian Smith55a3cf42015-08-09 17:08:49 -0400668
669 if err := copyFile(*outputDir, "doc.css"); err != nil {
670 fmt.Printf("Failed to copy static file: %s\n", err)
671 os.Exit(1)
672 }
Adam Langley95c29f32014-06-20 12:00:00 -0700673}