blob: 7c96af86c4ec2059b5c9296243874eb81789bddb [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 }
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
140func 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
193func 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
201func 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
221func (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
380func 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
395func 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
422func markupFirstWord(s template.HTML) template.HTML {
David Benjamin5b082e82014-12-26 00:54:52 -0500423 start := 0
424again:
425 end := strings.Index(string(s[start:]), " ")
426 if end > 0 {
427 end += start
428 w := strings.ToLower(string(s[start:end]))
429 if w == "a" || w == "an" || w == "deprecated:" {
430 start = end + 1
431 goto again
432 }
433 return s[:start] + "<span class=\"first-word\">" + s[start:end] + "</span>" + s[end:]
Adam Langley95c29f32014-06-20 12:00:00 -0700434 }
435 return s
436}
437
438func newlinesToBR(html template.HTML) template.HTML {
439 s := string(html)
440 if !strings.Contains(s, "\n") {
441 return html
442 }
443 s = strings.Replace(s, "\n", "<br>", -1)
444 s = strings.Replace(s, " ", "&nbsp;", -1)
445 return template.HTML(s)
446}
447
448func generate(outPath string, config *Config) (map[string]string, error) {
449 headerTmpl := template.New("headerTmpl")
450 headerTmpl.Funcs(template.FuncMap{
451 "firstSentence": firstSentence,
452 "markupPipeWords": markupPipeWords,
453 "markupFirstWord": markupFirstWord,
454 "newlinesToBR": newlinesToBR,
455 })
David Benjamin5b082e82014-12-26 00:54:52 -0500456 headerTmpl, err := headerTmpl.Parse(`<!DOCTYPE html>
Adam Langley95c29f32014-06-20 12:00:00 -0700457<html>
458 <head>
459 <title>BoringSSL - {{.Name}}</title>
460 <meta charset="utf-8">
461 <link rel="stylesheet" type="text/css" href="doc.css">
462 </head>
463
464 <body>
465 <div id="main">
466 <h2>{{.Name}}</h2>
467
468 {{range .Preamble}}<p>{{. | html | markupPipeWords}}</p>{{end}}
469
470 <ol>
471 {{range .Sections}}
472 {{if not .IsPrivate}}
David Benjamin5b082e82014-12-26 00:54:52 -0500473 {{if .Preamble}}<li class="header"><a href="#section-{{.Num}}">{{.Preamble | firstSentence | html | markupPipeWords}}</a></li>{{end}}
Adam Langley95c29f32014-06-20 12:00:00 -0700474 {{range .Decls}}
475 {{if .Name}}<li><a href="#decl-{{.Num}}"><tt>{{.Name}}</tt></a></li>{{end}}
476 {{end}}
477 {{end}}
478 {{end}}
479 </ol>
480
481 {{range .Sections}}
482 {{if not .IsPrivate}}
483 <div class="section">
484 {{if .Preamble}}
485 <div class="sectionpreamble">
486 <a name="section-{{.Num}}">
487 {{range .Preamble}}<p>{{. | html | markupPipeWords}}</p>{{end}}
488 </a>
489 </div>
490 {{end}}
491
492 {{range .Decls}}
493 <div class="decl">
494 <a name="decl-{{.Num}}">
495 {{range .Comment}}
496 <p>{{. | html | markupPipeWords | newlinesToBR | markupFirstWord}}</p>
497 {{end}}
498 <pre>{{.Decl}}</pre>
499 </a>
500 </div>
501 {{end}}
502 </div>
503 {{end}}
504 {{end}}
505 </div>
506 </body>
507</html>`)
508 if err != nil {
509 return nil, err
510 }
511
512 headerDescriptions := make(map[string]string)
513
514 for _, section := range config.Sections {
515 for _, headerPath := range section.Headers {
516 header, err := config.parseHeader(headerPath)
517 if err != nil {
518 return nil, errors.New("while parsing " + headerPath + ": " + err.Error())
519 }
520 headerDescriptions[header.Name] = firstSentence(header.Preamble)
521 filename := filepath.Join(outPath, header.Name+".html")
522 file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
523 if err != nil {
524 panic(err)
525 }
526 defer file.Close()
527 if err := headerTmpl.Execute(file, header); err != nil {
528 return nil, err
529 }
530 }
531 }
532
533 return headerDescriptions, nil
534}
535
536func generateIndex(outPath string, config *Config, headerDescriptions map[string]string) error {
537 indexTmpl := template.New("indexTmpl")
538 indexTmpl.Funcs(template.FuncMap{
539 "baseName": filepath.Base,
540 "headerDescription": func(header string) string {
541 return headerDescriptions[header]
542 },
543 })
544 indexTmpl, err := indexTmpl.Parse(`<!DOCTYPE html5>
545
546 <head>
547 <title>BoringSSL - Headers</title>
548 <meta charset="utf-8">
549 <link rel="stylesheet" type="text/css" href="doc.css">
550 </head>
551
552 <body>
553 <div id="main">
554 <table>
555 {{range .Sections}}
556 <tr class="header"><td colspan="2">{{.Name}}</td></tr>
557 {{range .Headers}}
558 <tr><td><a href="{{. | baseName}}.html">{{. | baseName}}</a></td><td>{{. | baseName | headerDescription}}</td></tr>
559 {{end}}
560 {{end}}
561 </table>
562 </div>
563 </body>
564</html>`)
565
566 if err != nil {
567 return err
568 }
569
570 file, err := os.OpenFile(filepath.Join(outPath, "headers.html"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
571 if err != nil {
572 panic(err)
573 }
574 defer file.Close()
575
576 if err := indexTmpl.Execute(file, config); err != nil {
577 return err
578 }
579
580 return nil
581}
582
583func main() {
584 var (
585 configFlag *string = flag.String("config", "", "Location of config file")
586 outputDir *string = flag.String("out", "", "Path to the directory where the output will be written")
587 config Config
588 )
589
590 flag.Parse()
591
592 if len(*configFlag) == 0 {
593 fmt.Printf("No config file given by --config\n")
594 os.Exit(1)
595 }
596
597 if len(*outputDir) == 0 {
598 fmt.Printf("No output directory given by --out\n")
599 os.Exit(1)
600 }
601
602 configBytes, err := ioutil.ReadFile(*configFlag)
603 if err != nil {
604 fmt.Printf("Failed to open config file: %s\n", err)
605 os.Exit(1)
606 }
607
608 if err := json.Unmarshal(configBytes, &config); err != nil {
609 fmt.Printf("Failed to parse config file: %s\n", err)
610 os.Exit(1)
611 }
612
613 headerDescriptions, err := generate(*outputDir, &config)
614 if err != nil {
615 fmt.Printf("Failed to generate output: %s\n", err)
616 os.Exit(1)
617 }
618
619 if err := generateIndex(*outputDir, &config, headerDescriptions); err != nil {
620 fmt.Printf("Failed to generate index: %s\n", err)
621 os.Exit(1)
622 }
623}