blob: 0ab6f892abc71b2d6d563c16a2f0a599e1b3227b [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
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
59type 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
74const (
75 cppGuard = "#if defined(__cplusplus)"
76 commentStart = "/* "
77 commentEnd = " */"
78)
79
80func 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 }
David Benjamin48b31502015-04-08 23:17:55 -0400114 if len(line) == 2 || line[2] != '/' {
115 line = line[2:]
116 }
Adam Langley95c29f32014-06-20 12:00:00 -0700117 if strings.HasPrefix(line, " ") {
118 /* Identing the lines of a paragraph marks them as
119 * preformatted. */
120 if len(commentParagraph) > 0 {
121 commentParagraph += "\n"
122 }
123 line = line[3:]
124 }
125 if len(line) > 0 {
126 commentParagraph = commentParagraph + line
127 if len(commentParagraph) > 0 && commentParagraph[0] == ' ' {
128 commentParagraph = commentParagraph[1:]
129 }
130 } else {
131 comment = append(comment, commentParagraph)
132 commentParagraph = ""
133 }
134 rest = rest[1:]
135 restLineNo++
136 }
137
138 err = errors.New("hit EOF in comment")
139 return
140}
141
142func extractDecl(lines []string, lineNo int) (decl string, rest []string, restLineNo int, err error) {
143 if len(lines) == 0 {
144 return "", lines, lineNo, nil
145 }
146
147 rest = lines
148 restLineNo = lineNo
149
150 var stack []rune
151 for len(rest) > 0 {
152 line := rest[0]
153 for _, c := range line {
154 switch c {
155 case '(', '{', '[':
156 stack = append(stack, c)
157 case ')', '}', ']':
158 if len(stack) == 0 {
159 err = fmt.Errorf("unexpected %c on line %d", c, restLineNo)
160 return
161 }
162 var expected rune
163 switch c {
164 case ')':
165 expected = '('
166 case '}':
167 expected = '{'
168 case ']':
169 expected = '['
170 default:
171 panic("internal error")
172 }
173 if last := stack[len(stack)-1]; last != expected {
174 err = fmt.Errorf("found %c when expecting %c on line %d", c, last, restLineNo)
175 return
176 }
177 stack = stack[:len(stack)-1]
178 }
179 }
180 if len(decl) > 0 {
181 decl += "\n"
182 }
183 decl += line
184 rest = rest[1:]
185 restLineNo++
186
187 if len(stack) == 0 && (len(decl) == 0 || decl[len(decl)-1] != '\\') {
188 break
189 }
190 }
191
192 return
193}
194
195func skipPast(s, skip string) string {
196 i := strings.Index(s, skip)
197 if i > 0 {
David Benjamin48b31502015-04-08 23:17:55 -0400198 return s[i:]
Adam Langley95c29f32014-06-20 12:00:00 -0700199 }
200 return s
201}
202
David Benjamin71485af2015-04-09 00:06:03 -0400203func skipLine(s string) string {
204 i := strings.Index(s, "\n")
205 if i > 0 {
206 return s[i:]
207 }
208 return ""
209}
210
Adam Langley95c29f32014-06-20 12:00:00 -0700211func getNameFromDecl(decl string) (string, bool) {
David Benjaminb4804282015-05-16 12:12:31 -0400212 for strings.HasPrefix(decl, "#if") || strings.HasPrefix(decl, "#elif") {
David Benjamin71485af2015-04-09 00:06:03 -0400213 decl = skipLine(decl)
214 }
Adam Langley95c29f32014-06-20 12:00:00 -0700215 if strings.HasPrefix(decl, "struct ") {
216 return "", false
217 }
David Benjamin6deacb32015-05-16 12:00:51 -0400218 if strings.HasPrefix(decl, "#define ") {
219 // This is a preprocessor #define. The name is the next symbol.
220 decl = strings.TrimPrefix(decl, "#define ")
221 for len(decl) > 0 && decl[0] == ' ' {
222 decl = decl[1:]
223 }
224 i := strings.IndexAny(decl, "( ")
225 if i < 0 {
226 return "", false
227 }
228 return decl[:i], true
229 }
Adam Langley95c29f32014-06-20 12:00:00 -0700230 decl = skipPast(decl, "STACK_OF(")
231 decl = skipPast(decl, "LHASH_OF(")
232 i := strings.Index(decl, "(")
233 if i < 0 {
234 return "", false
235 }
236 j := strings.LastIndex(decl[:i], " ")
237 if j < 0 {
238 return "", false
239 }
240 for j+1 < len(decl) && decl[j+1] == '*' {
241 j++
242 }
243 return decl[j+1 : i], true
244}
245
246func (config *Config) parseHeader(path string) (*HeaderFile, error) {
247 headerPath := filepath.Join(config.BaseDirectory, path)
248
249 headerFile, err := os.Open(headerPath)
250 if err != nil {
251 return nil, err
252 }
253 defer headerFile.Close()
254
255 scanner := bufio.NewScanner(headerFile)
256 var lines, oldLines []string
257 for scanner.Scan() {
258 lines = append(lines, scanner.Text())
259 }
260 if err := scanner.Err(); err != nil {
261 return nil, err
262 }
263
264 lineNo := 0
265 found := false
266 for i, line := range lines {
267 lineNo++
268 if line == cppGuard {
269 lines = lines[i+1:]
270 lineNo++
271 found = true
272 break
273 }
274 }
275
276 if !found {
277 return nil, errors.New("no C++ guard found")
278 }
279
280 if len(lines) == 0 || lines[0] != "extern \"C\" {" {
281 return nil, errors.New("no extern \"C\" found after C++ guard")
282 }
283 lineNo += 2
284 lines = lines[2:]
285
286 header := &HeaderFile{
287 Name: filepath.Base(path),
288 }
289
290 for i, line := range lines {
291 lineNo++
292 if len(line) > 0 {
293 lines = lines[i:]
294 break
295 }
296 }
297
298 oldLines = lines
299 if len(lines) > 0 && strings.HasPrefix(lines[0], commentStart) {
300 comment, rest, restLineNo, err := extractComment(lines, lineNo)
301 if err != nil {
302 return nil, err
303 }
304
305 if len(rest) > 0 && len(rest[0]) == 0 {
306 if len(rest) < 2 || len(rest[1]) != 0 {
307 return nil, errors.New("preamble comment should be followed by two blank lines")
308 }
309 header.Preamble = comment
310 lineNo = restLineNo + 2
311 lines = rest[2:]
312 } else {
313 lines = oldLines
314 }
315 }
316
317 var sectionNumber, declNumber int
318
319 for {
320 // Start of a section.
321 if len(lines) == 0 {
322 return nil, errors.New("unexpected end of file")
323 }
324 line := lines[0]
325 if line == cppGuard {
326 break
327 }
328
329 if len(line) == 0 {
330 return nil, fmt.Errorf("blank line at start of section on line %d", lineNo)
331 }
332
333 section := HeaderSection{
334 Num: sectionNumber,
335 }
336 sectionNumber++
337
338 if strings.HasPrefix(line, commentStart) {
339 comment, rest, restLineNo, err := extractComment(lines, lineNo)
340 if err != nil {
341 return nil, err
342 }
343 if len(rest) > 0 && len(rest[0]) == 0 {
344 section.Preamble = comment
345 section.IsPrivate = len(comment) > 0 && strings.HasPrefix(comment[0], "Private functions")
346 lines = rest[1:]
347 lineNo = restLineNo + 1
348 }
349 }
350
351 for len(lines) > 0 {
352 line := lines[0]
353 if len(line) == 0 {
354 lines = lines[1:]
355 lineNo++
356 break
357 }
358 if line == cppGuard {
359 return nil, errors.New("hit ending C++ guard while in section")
360 }
361
362 var comment []string
363 var decl string
364 if strings.HasPrefix(line, commentStart) {
365 comment, lines, lineNo, err = extractComment(lines, lineNo)
366 if err != nil {
367 return nil, err
368 }
369 }
370 if len(lines) == 0 {
371 return nil, errors.New("expected decl at EOF")
372 }
373 decl, lines, lineNo, err = extractDecl(lines, lineNo)
374 if err != nil {
375 return nil, err
376 }
377 name, ok := getNameFromDecl(decl)
378 if !ok {
379 name = ""
380 }
381 if last := len(section.Decls) - 1; len(name) == 0 && len(comment) == 0 && last >= 0 {
382 section.Decls[last].Decl += "\n" + decl
383 } else {
384 section.Decls = append(section.Decls, HeaderDecl{
385 Comment: comment,
386 Name: name,
387 Decl: decl,
388 Num: declNumber,
389 })
390 declNumber++
391 }
392
393 if len(lines) > 0 && len(lines[0]) == 0 {
394 lines = lines[1:]
395 lineNo++
396 }
397 }
398
399 header.Sections = append(header.Sections, section)
400 }
401
402 return header, nil
403}
404
405func firstSentence(paragraphs []string) string {
406 if len(paragraphs) == 0 {
407 return ""
408 }
409 s := paragraphs[0]
410 i := strings.Index(s, ". ")
411 if i >= 0 {
412 return s[:i]
413 }
414 if lastIndex := len(s) - 1; s[lastIndex] == '.' {
415 return s[:lastIndex]
416 }
417 return s
418}
419
420func markupPipeWords(s string) template.HTML {
421 ret := ""
422
423 for {
424 i := strings.Index(s, "|")
425 if i == -1 {
426 ret += s
427 break
428 }
429 ret += s[:i]
430 s = s[i+1:]
431
432 i = strings.Index(s, "|")
433 j := strings.Index(s, " ")
434 if i > 0 && (j == -1 || j > i) {
435 ret += "<tt>"
436 ret += s[:i]
437 ret += "</tt>"
438 s = s[i+1:]
439 } else {
440 ret += "|"
441 }
442 }
443
444 return template.HTML(ret)
445}
446
447func markupFirstWord(s template.HTML) template.HTML {
David Benjamin5b082e82014-12-26 00:54:52 -0500448 start := 0
449again:
450 end := strings.Index(string(s[start:]), " ")
451 if end > 0 {
452 end += start
453 w := strings.ToLower(string(s[start:end]))
454 if w == "a" || w == "an" || w == "deprecated:" {
455 start = end + 1
456 goto again
457 }
458 return s[:start] + "<span class=\"first-word\">" + s[start:end] + "</span>" + s[end:]
Adam Langley95c29f32014-06-20 12:00:00 -0700459 }
460 return s
461}
462
463func newlinesToBR(html template.HTML) template.HTML {
464 s := string(html)
465 if !strings.Contains(s, "\n") {
466 return html
467 }
468 s = strings.Replace(s, "\n", "<br>", -1)
469 s = strings.Replace(s, " ", "&nbsp;", -1)
470 return template.HTML(s)
471}
472
473func generate(outPath string, config *Config) (map[string]string, error) {
474 headerTmpl := template.New("headerTmpl")
475 headerTmpl.Funcs(template.FuncMap{
476 "firstSentence": firstSentence,
477 "markupPipeWords": markupPipeWords,
478 "markupFirstWord": markupFirstWord,
479 "newlinesToBR": newlinesToBR,
480 })
David Benjamin5b082e82014-12-26 00:54:52 -0500481 headerTmpl, err := headerTmpl.Parse(`<!DOCTYPE html>
Adam Langley95c29f32014-06-20 12:00:00 -0700482<html>
483 <head>
484 <title>BoringSSL - {{.Name}}</title>
485 <meta charset="utf-8">
486 <link rel="stylesheet" type="text/css" href="doc.css">
487 </head>
488
489 <body>
490 <div id="main">
491 <h2>{{.Name}}</h2>
492
493 {{range .Preamble}}<p>{{. | html | markupPipeWords}}</p>{{end}}
494
495 <ol>
496 {{range .Sections}}
497 {{if not .IsPrivate}}
David Benjamin5b082e82014-12-26 00:54:52 -0500498 {{if .Preamble}}<li class="header"><a href="#section-{{.Num}}">{{.Preamble | firstSentence | html | markupPipeWords}}</a></li>{{end}}
Adam Langley95c29f32014-06-20 12:00:00 -0700499 {{range .Decls}}
500 {{if .Name}}<li><a href="#decl-{{.Num}}"><tt>{{.Name}}</tt></a></li>{{end}}
501 {{end}}
502 {{end}}
503 {{end}}
504 </ol>
505
506 {{range .Sections}}
507 {{if not .IsPrivate}}
508 <div class="section">
509 {{if .Preamble}}
510 <div class="sectionpreamble">
511 <a name="section-{{.Num}}">
512 {{range .Preamble}}<p>{{. | html | markupPipeWords}}</p>{{end}}
513 </a>
514 </div>
515 {{end}}
516
517 {{range .Decls}}
518 <div class="decl">
519 <a name="decl-{{.Num}}">
520 {{range .Comment}}
521 <p>{{. | html | markupPipeWords | newlinesToBR | markupFirstWord}}</p>
522 {{end}}
523 <pre>{{.Decl}}</pre>
524 </a>
525 </div>
526 {{end}}
527 </div>
528 {{end}}
529 {{end}}
530 </div>
531 </body>
532</html>`)
533 if err != nil {
534 return nil, err
535 }
536
537 headerDescriptions := make(map[string]string)
538
539 for _, section := range config.Sections {
540 for _, headerPath := range section.Headers {
541 header, err := config.parseHeader(headerPath)
542 if err != nil {
543 return nil, errors.New("while parsing " + headerPath + ": " + err.Error())
544 }
545 headerDescriptions[header.Name] = firstSentence(header.Preamble)
546 filename := filepath.Join(outPath, header.Name+".html")
547 file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
548 if err != nil {
549 panic(err)
550 }
551 defer file.Close()
552 if err := headerTmpl.Execute(file, header); err != nil {
553 return nil, err
554 }
555 }
556 }
557
558 return headerDescriptions, nil
559}
560
561func generateIndex(outPath string, config *Config, headerDescriptions map[string]string) error {
562 indexTmpl := template.New("indexTmpl")
563 indexTmpl.Funcs(template.FuncMap{
564 "baseName": filepath.Base,
565 "headerDescription": func(header string) string {
566 return headerDescriptions[header]
567 },
568 })
569 indexTmpl, err := indexTmpl.Parse(`<!DOCTYPE html5>
570
571 <head>
572 <title>BoringSSL - Headers</title>
573 <meta charset="utf-8">
574 <link rel="stylesheet" type="text/css" href="doc.css">
575 </head>
576
577 <body>
578 <div id="main">
579 <table>
580 {{range .Sections}}
581 <tr class="header"><td colspan="2">{{.Name}}</td></tr>
582 {{range .Headers}}
583 <tr><td><a href="{{. | baseName}}.html">{{. | baseName}}</a></td><td>{{. | baseName | headerDescription}}</td></tr>
584 {{end}}
585 {{end}}
586 </table>
587 </div>
588 </body>
589</html>`)
590
591 if err != nil {
592 return err
593 }
594
595 file, err := os.OpenFile(filepath.Join(outPath, "headers.html"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
596 if err != nil {
597 panic(err)
598 }
599 defer file.Close()
600
601 if err := indexTmpl.Execute(file, config); err != nil {
602 return err
603 }
604
605 return nil
606}
607
Brian Smith55a3cf42015-08-09 17:08:49 -0400608func copyFile(outPath string, inFilePath string) error {
609 bytes, err := ioutil.ReadFile(inFilePath)
610 if err != nil {
611 return err
612 }
613 return ioutil.WriteFile(filepath.Join(outPath, filepath.Base(inFilePath)), bytes, 0666)
614}
615
Adam Langley95c29f32014-06-20 12:00:00 -0700616func main() {
617 var (
Adam Langley0fd56392015-04-08 17:32:55 -0700618 configFlag *string = flag.String("config", "doc.config", "Location of config file")
619 outputDir *string = flag.String("out", ".", "Path to the directory where the output will be written")
Adam Langley95c29f32014-06-20 12:00:00 -0700620 config Config
621 )
622
623 flag.Parse()
624
625 if len(*configFlag) == 0 {
626 fmt.Printf("No config file given by --config\n")
627 os.Exit(1)
628 }
629
630 if len(*outputDir) == 0 {
631 fmt.Printf("No output directory given by --out\n")
632 os.Exit(1)
633 }
634
635 configBytes, err := ioutil.ReadFile(*configFlag)
636 if err != nil {
637 fmt.Printf("Failed to open config file: %s\n", err)
638 os.Exit(1)
639 }
640
641 if err := json.Unmarshal(configBytes, &config); err != nil {
642 fmt.Printf("Failed to parse config file: %s\n", err)
643 os.Exit(1)
644 }
645
646 headerDescriptions, err := generate(*outputDir, &config)
647 if err != nil {
648 fmt.Printf("Failed to generate output: %s\n", err)
649 os.Exit(1)
650 }
651
652 if err := generateIndex(*outputDir, &config, headerDescriptions); err != nil {
653 fmt.Printf("Failed to generate index: %s\n", err)
654 os.Exit(1)
655 }
Brian Smith55a3cf42015-08-09 17:08:49 -0400656
657 if err := copyFile(*outputDir, "doc.css"); err != nil {
658 fmt.Printf("Failed to copy static file: %s\n", err)
659 os.Exit(1)
660 }
Adam Langley95c29f32014-06-20 12:00:00 -0700661}