blob: ce7a3e80f392ee4d6e8d8ffea03ddb830e10c914 [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
David Benjamindfa9c4a2015-10-18 01:08:11 -040045 // AllDecls maps all decls to their URL fragments.
46 AllDecls map[string]string
Adam Langley95c29f32014-06-20 12:00:00 -070047}
48
49type HeaderSection struct {
50 // Preamble contains a comment for a group of functions.
51 Preamble []string
52 Decls []HeaderDecl
David Benjamin1bfce802015-09-07 13:21:08 -040053 // Anchor, if non-empty, is the URL fragment to use in anchor tags.
54 Anchor string
Adam Langley95c29f32014-06-20 12:00:00 -070055 // IsPrivate is true if the section contains private functions (as
56 // indicated by its name).
57 IsPrivate bool
58}
59
60type HeaderDecl struct {
61 // Comment contains a comment for a specific function. Each string is a
62 // paragraph. Some paragraph may contain \n runes to indicate that they
63 // are preformatted.
64 Comment []string
65 // Name contains the name of the function, if it could be extracted.
66 Name string
67 // Decl contains the preformatted C declaration itself.
68 Decl string
David Benjamin1bfce802015-09-07 13:21:08 -040069 // Anchor, if non-empty, is the URL fragment to use in anchor tags.
70 Anchor string
Adam Langley95c29f32014-06-20 12:00:00 -070071}
72
73const (
74 cppGuard = "#if defined(__cplusplus)"
75 commentStart = "/* "
76 commentEnd = " */"
77)
78
79func extractComment(lines []string, lineNo int) (comment []string, rest []string, restLineNo int, err error) {
80 if len(lines) == 0 {
81 return nil, lines, lineNo, nil
82 }
83
84 restLineNo = lineNo
85 rest = lines
86
87 if !strings.HasPrefix(rest[0], commentStart) {
88 panic("extractComment called on non-comment")
89 }
90 commentParagraph := rest[0][len(commentStart):]
91 rest = rest[1:]
92 restLineNo++
93
94 for len(rest) > 0 {
95 i := strings.Index(commentParagraph, commentEnd)
96 if i >= 0 {
97 if i != len(commentParagraph)-len(commentEnd) {
98 err = fmt.Errorf("garbage after comment end on line %d", restLineNo)
99 return
100 }
101 commentParagraph = commentParagraph[:i]
102 if len(commentParagraph) > 0 {
103 comment = append(comment, commentParagraph)
104 }
105 return
106 }
107
108 line := rest[0]
109 if !strings.HasPrefix(line, " *") {
110 err = fmt.Errorf("comment doesn't start with block prefix on line %d: %s", restLineNo, line)
111 return
112 }
David Benjamin48b31502015-04-08 23:17:55 -0400113 if len(line) == 2 || line[2] != '/' {
114 line = line[2:]
115 }
Adam Langley95c29f32014-06-20 12:00:00 -0700116 if strings.HasPrefix(line, " ") {
117 /* Identing the lines of a paragraph marks them as
118 * preformatted. */
119 if len(commentParagraph) > 0 {
120 commentParagraph += "\n"
121 }
122 line = line[3:]
123 }
124 if len(line) > 0 {
125 commentParagraph = commentParagraph + line
126 if len(commentParagraph) > 0 && commentParagraph[0] == ' ' {
127 commentParagraph = commentParagraph[1:]
128 }
129 } else {
130 comment = append(comment, commentParagraph)
131 commentParagraph = ""
132 }
133 rest = rest[1:]
134 restLineNo++
135 }
136
137 err = errors.New("hit EOF in comment")
138 return
139}
140
141func extractDecl(lines []string, lineNo int) (decl string, rest []string, restLineNo int, err error) {
142 if len(lines) == 0 {
143 return "", lines, lineNo, nil
144 }
145
146 rest = lines
147 restLineNo = lineNo
148
149 var stack []rune
150 for len(rest) > 0 {
151 line := rest[0]
152 for _, c := range line {
153 switch c {
154 case '(', '{', '[':
155 stack = append(stack, c)
156 case ')', '}', ']':
157 if len(stack) == 0 {
158 err = fmt.Errorf("unexpected %c on line %d", c, restLineNo)
159 return
160 }
161 var expected rune
162 switch c {
163 case ')':
164 expected = '('
165 case '}':
166 expected = '{'
167 case ']':
168 expected = '['
169 default:
170 panic("internal error")
171 }
172 if last := stack[len(stack)-1]; last != expected {
173 err = fmt.Errorf("found %c when expecting %c on line %d", c, last, restLineNo)
174 return
175 }
176 stack = stack[:len(stack)-1]
177 }
178 }
179 if len(decl) > 0 {
180 decl += "\n"
181 }
182 decl += line
183 rest = rest[1:]
184 restLineNo++
185
186 if len(stack) == 0 && (len(decl) == 0 || decl[len(decl)-1] != '\\') {
187 break
188 }
189 }
190
191 return
192}
193
David Benjamin71485af2015-04-09 00:06:03 -0400194func skipLine(s string) string {
195 i := strings.Index(s, "\n")
196 if i > 0 {
197 return s[i:]
198 }
199 return ""
200}
201
Adam Langley95c29f32014-06-20 12:00:00 -0700202func getNameFromDecl(decl string) (string, bool) {
David Benjaminb4804282015-05-16 12:12:31 -0400203 for strings.HasPrefix(decl, "#if") || strings.HasPrefix(decl, "#elif") {
David Benjamin71485af2015-04-09 00:06:03 -0400204 decl = skipLine(decl)
205 }
Adam Langley95c29f32014-06-20 12:00:00 -0700206 if strings.HasPrefix(decl, "struct ") {
207 return "", false
208 }
David Benjamin6deacb32015-05-16 12:00:51 -0400209 if strings.HasPrefix(decl, "#define ") {
210 // This is a preprocessor #define. The name is the next symbol.
211 decl = strings.TrimPrefix(decl, "#define ")
212 for len(decl) > 0 && decl[0] == ' ' {
213 decl = decl[1:]
214 }
215 i := strings.IndexAny(decl, "( ")
216 if i < 0 {
217 return "", false
218 }
219 return decl[:i], true
220 }
David Benjamin361ecc02015-09-13 01:16:50 -0400221 decl = strings.TrimPrefix(decl, "OPENSSL_EXPORT ")
222 decl = strings.TrimPrefix(decl, "STACK_OF(")
223 decl = strings.TrimPrefix(decl, "LHASH_OF(")
Adam Langley95c29f32014-06-20 12:00:00 -0700224 i := strings.Index(decl, "(")
225 if i < 0 {
226 return "", false
227 }
228 j := strings.LastIndex(decl[:i], " ")
229 if j < 0 {
230 return "", false
231 }
232 for j+1 < len(decl) && decl[j+1] == '*' {
233 j++
234 }
235 return decl[j+1 : i], true
236}
237
David Benjamin1bfce802015-09-07 13:21:08 -0400238func sanitizeAnchor(name string) string {
239 return strings.Replace(name, " ", "-", -1)
240}
241
David Benjamin5ef619e2015-10-18 00:10:28 -0400242func isPrivateSection(name string) bool {
243 return strings.HasPrefix(name, "Private functions") || strings.HasPrefix(name, "Private structures") || strings.Contains(name, "(hidden)")
244}
245
Adam Langley95c29f32014-06-20 12:00:00 -0700246func (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{
David Benjamindfa9c4a2015-10-18 01:08:11 -0400287 Name: filepath.Base(path),
288 AllDecls: make(map[string]string),
Adam Langley95c29f32014-06-20 12:00:00 -0700289 }
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
David Benjamin5ef619e2015-10-18 00:10:28 -0400351 section.IsPrivate = len(comment) > 0 && isPrivateSection(comment[0])
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
David Benjamindfa9c4a2015-10-18 01:08:11 -0400397 header.AllDecls[name] = anchor
398
Adam Langley95c29f32014-06-20 12:00:00 -0700399 section.Decls = append(section.Decls, HeaderDecl{
400 Comment: comment,
401 Name: name,
402 Decl: decl,
David Benjamin1bfce802015-09-07 13:21:08 -0400403 Anchor: anchor,
Adam Langley95c29f32014-06-20 12:00:00 -0700404 })
Adam Langley95c29f32014-06-20 12:00:00 -0700405 }
406
407 if len(lines) > 0 && len(lines[0]) == 0 {
408 lines = lines[1:]
409 lineNo++
410 }
411 }
412
413 header.Sections = append(header.Sections, section)
414 }
415
416 return header, nil
417}
418
419func firstSentence(paragraphs []string) string {
420 if len(paragraphs) == 0 {
421 return ""
422 }
423 s := paragraphs[0]
424 i := strings.Index(s, ". ")
425 if i >= 0 {
426 return s[:i]
427 }
428 if lastIndex := len(s) - 1; s[lastIndex] == '.' {
429 return s[:lastIndex]
430 }
431 return s
432}
433
David Benjamindfa9c4a2015-10-18 01:08:11 -0400434func markupPipeWords(allDecls map[string]string, s string) template.HTML {
Adam Langley95c29f32014-06-20 12:00:00 -0700435 ret := ""
436
437 for {
438 i := strings.Index(s, "|")
439 if i == -1 {
440 ret += s
441 break
442 }
443 ret += s[:i]
444 s = s[i+1:]
445
446 i = strings.Index(s, "|")
447 j := strings.Index(s, " ")
448 if i > 0 && (j == -1 || j > i) {
449 ret += "<tt>"
David Benjamindfa9c4a2015-10-18 01:08:11 -0400450 anchor, isLink := allDecls[s[:i]]
451 if isLink {
452 ret += fmt.Sprintf("<a href=\"%s\">", template.HTMLEscapeString(anchor))
453 }
Adam Langley95c29f32014-06-20 12:00:00 -0700454 ret += s[:i]
David Benjamindfa9c4a2015-10-18 01:08:11 -0400455 if isLink {
456 ret += "</a>"
457 }
Adam Langley95c29f32014-06-20 12:00:00 -0700458 ret += "</tt>"
459 s = s[i+1:]
460 } else {
461 ret += "|"
462 }
463 }
464
465 return template.HTML(ret)
466}
467
468func markupFirstWord(s template.HTML) template.HTML {
David Benjamin5b082e82014-12-26 00:54:52 -0500469 start := 0
470again:
471 end := strings.Index(string(s[start:]), " ")
472 if end > 0 {
473 end += start
474 w := strings.ToLower(string(s[start:end]))
David Benjamindfa9c4a2015-10-18 01:08:11 -0400475 // The first word was already marked up as an HTML tag. Don't
476 // mark it up further.
477 if strings.ContainsRune(w, '<') {
478 return s
479 }
David Benjamin7e40d4e2015-09-07 13:17:45 -0400480 if w == "a" || w == "an" {
David Benjamin5b082e82014-12-26 00:54:52 -0500481 start = end + 1
482 goto again
483 }
484 return s[:start] + "<span class=\"first-word\">" + s[start:end] + "</span>" + s[end:]
Adam Langley95c29f32014-06-20 12:00:00 -0700485 }
486 return s
487}
488
489func newlinesToBR(html template.HTML) template.HTML {
490 s := string(html)
491 if !strings.Contains(s, "\n") {
492 return html
493 }
494 s = strings.Replace(s, "\n", "<br>", -1)
495 s = strings.Replace(s, " ", "&nbsp;", -1)
496 return template.HTML(s)
497}
498
499func generate(outPath string, config *Config) (map[string]string, error) {
David Benjamindfa9c4a2015-10-18 01:08:11 -0400500 allDecls := make(map[string]string)
501
Adam Langley95c29f32014-06-20 12:00:00 -0700502 headerTmpl := template.New("headerTmpl")
503 headerTmpl.Funcs(template.FuncMap{
504 "firstSentence": firstSentence,
David Benjamindfa9c4a2015-10-18 01:08:11 -0400505 "markupPipeWords": func(s string) template.HTML { return markupPipeWords(allDecls, s) },
Adam Langley95c29f32014-06-20 12:00:00 -0700506 "markupFirstWord": markupFirstWord,
507 "newlinesToBR": newlinesToBR,
508 })
David Benjamin5b082e82014-12-26 00:54:52 -0500509 headerTmpl, err := headerTmpl.Parse(`<!DOCTYPE html>
Adam Langley95c29f32014-06-20 12:00:00 -0700510<html>
511 <head>
512 <title>BoringSSL - {{.Name}}</title>
513 <meta charset="utf-8">
514 <link rel="stylesheet" type="text/css" href="doc.css">
515 </head>
516
517 <body>
518 <div id="main">
519 <h2>{{.Name}}</h2>
520
521 {{range .Preamble}}<p>{{. | html | markupPipeWords}}</p>{{end}}
522
523 <ol>
524 {{range .Sections}}
525 {{if not .IsPrivate}}
David Benjamin1bfce802015-09-07 13:21:08 -0400526 {{if .Anchor}}<li class="header"><a href="#{{.Anchor}}">{{.Preamble | firstSentence | html | markupPipeWords}}</a></li>{{end}}
Adam Langley95c29f32014-06-20 12:00:00 -0700527 {{range .Decls}}
David Benjamin1bfce802015-09-07 13:21:08 -0400528 {{if .Anchor}}<li><a href="#{{.Anchor}}"><tt>{{.Name}}</tt></a></li>{{end}}
Adam Langley95c29f32014-06-20 12:00:00 -0700529 {{end}}
530 {{end}}
531 {{end}}
532 </ol>
533
534 {{range .Sections}}
535 {{if not .IsPrivate}}
David Benjamindfa9c4a2015-10-18 01:08:11 -0400536 <div class="section" {{if .Anchor}}id="{{.Anchor}}"{{end}}>
Adam Langley95c29f32014-06-20 12:00:00 -0700537 {{if .Preamble}}
538 <div class="sectionpreamble">
Adam Langley95c29f32014-06-20 12:00:00 -0700539 {{range .Preamble}}<p>{{. | html | markupPipeWords}}</p>{{end}}
Adam Langley95c29f32014-06-20 12:00:00 -0700540 </div>
541 {{end}}
542
543 {{range .Decls}}
David Benjamindfa9c4a2015-10-18 01:08:11 -0400544 <div class="decl" {{if .Anchor}}id="{{.Anchor}}"{{end}}>
Adam Langley95c29f32014-06-20 12:00:00 -0700545 {{range .Comment}}
546 <p>{{. | html | markupPipeWords | newlinesToBR | markupFirstWord}}</p>
547 {{end}}
548 <pre>{{.Decl}}</pre>
Adam Langley95c29f32014-06-20 12:00:00 -0700549 </div>
550 {{end}}
551 </div>
552 {{end}}
553 {{end}}
554 </div>
555 </body>
556</html>`)
557 if err != nil {
558 return nil, err
559 }
560
561 headerDescriptions := make(map[string]string)
David Benjamindfa9c4a2015-10-18 01:08:11 -0400562 var headers []*HeaderFile
Adam Langley95c29f32014-06-20 12:00:00 -0700563
564 for _, section := range config.Sections {
565 for _, headerPath := range section.Headers {
566 header, err := config.parseHeader(headerPath)
567 if err != nil {
568 return nil, errors.New("while parsing " + headerPath + ": " + err.Error())
569 }
570 headerDescriptions[header.Name] = firstSentence(header.Preamble)
David Benjamindfa9c4a2015-10-18 01:08:11 -0400571 headers = append(headers, header)
572
573 for name, anchor := range header.AllDecls {
574 allDecls[name] = fmt.Sprintf("%s#%s", header.Name+".html", anchor)
Adam Langley95c29f32014-06-20 12:00:00 -0700575 }
David Benjamindfa9c4a2015-10-18 01:08:11 -0400576 }
577 }
578
579 for _, header := range headers {
580 filename := filepath.Join(outPath, header.Name+".html")
581 file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
582 if err != nil {
583 panic(err)
584 }
585 defer file.Close()
586 if err := headerTmpl.Execute(file, header); err != nil {
587 return nil, err
Adam Langley95c29f32014-06-20 12:00:00 -0700588 }
589 }
590
591 return headerDescriptions, nil
592}
593
594func generateIndex(outPath string, config *Config, headerDescriptions map[string]string) error {
595 indexTmpl := template.New("indexTmpl")
596 indexTmpl.Funcs(template.FuncMap{
597 "baseName": filepath.Base,
598 "headerDescription": func(header string) string {
599 return headerDescriptions[header]
600 },
601 })
602 indexTmpl, err := indexTmpl.Parse(`<!DOCTYPE html5>
603
604 <head>
605 <title>BoringSSL - Headers</title>
606 <meta charset="utf-8">
607 <link rel="stylesheet" type="text/css" href="doc.css">
608 </head>
609
610 <body>
611 <div id="main">
612 <table>
613 {{range .Sections}}
614 <tr class="header"><td colspan="2">{{.Name}}</td></tr>
615 {{range .Headers}}
616 <tr><td><a href="{{. | baseName}}.html">{{. | baseName}}</a></td><td>{{. | baseName | headerDescription}}</td></tr>
617 {{end}}
618 {{end}}
619 </table>
620 </div>
621 </body>
622</html>`)
623
624 if err != nil {
625 return err
626 }
627
628 file, err := os.OpenFile(filepath.Join(outPath, "headers.html"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
629 if err != nil {
630 panic(err)
631 }
632 defer file.Close()
633
634 if err := indexTmpl.Execute(file, config); err != nil {
635 return err
636 }
637
638 return nil
639}
640
Brian Smith55a3cf42015-08-09 17:08:49 -0400641func copyFile(outPath string, inFilePath string) error {
642 bytes, err := ioutil.ReadFile(inFilePath)
643 if err != nil {
644 return err
645 }
646 return ioutil.WriteFile(filepath.Join(outPath, filepath.Base(inFilePath)), bytes, 0666)
647}
648
Adam Langley95c29f32014-06-20 12:00:00 -0700649func main() {
650 var (
Adam Langley0fd56392015-04-08 17:32:55 -0700651 configFlag *string = flag.String("config", "doc.config", "Location of config file")
652 outputDir *string = flag.String("out", ".", "Path to the directory where the output will be written")
Adam Langley95c29f32014-06-20 12:00:00 -0700653 config Config
654 )
655
656 flag.Parse()
657
658 if len(*configFlag) == 0 {
659 fmt.Printf("No config file given by --config\n")
660 os.Exit(1)
661 }
662
663 if len(*outputDir) == 0 {
664 fmt.Printf("No output directory given by --out\n")
665 os.Exit(1)
666 }
667
668 configBytes, err := ioutil.ReadFile(*configFlag)
669 if err != nil {
670 fmt.Printf("Failed to open config file: %s\n", err)
671 os.Exit(1)
672 }
673
674 if err := json.Unmarshal(configBytes, &config); err != nil {
675 fmt.Printf("Failed to parse config file: %s\n", err)
676 os.Exit(1)
677 }
678
679 headerDescriptions, err := generate(*outputDir, &config)
680 if err != nil {
681 fmt.Printf("Failed to generate output: %s\n", err)
682 os.Exit(1)
683 }
684
685 if err := generateIndex(*outputDir, &config, headerDescriptions); err != nil {
686 fmt.Printf("Failed to generate index: %s\n", err)
687 os.Exit(1)
688 }
Brian Smith55a3cf42015-08-09 17:08:49 -0400689
690 if err := copyFile(*outputDir, "doc.css"); err != nil {
691 fmt.Printf("Failed to copy static file: %s\n", err)
692 os.Exit(1)
693 }
Adam Langley95c29f32014-06-20 12:00:00 -0700694}