1// 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 "regexp" 21 "strings" 22) 23 24// Config describes the structure of the config JSON file. 25type Config struct { 26 // BaseDirectory is a path to which other paths in the file are 27 // relative. 28 BaseDirectory string 29 Sections []ConfigSection 30} 31 32type ConfigSection struct { 33 Name string 34 // Headers is a list of paths to header files. 35 Headers []string 36} 37 38// HeaderFile is the internal representation of a header file. 39type HeaderFile struct { 40 // Name is the basename of the header file (e.g. "ex_data.html"). 41 Name string 42 // Preamble contains a comment for the file as a whole. Each string 43 // is a separate paragraph. 44 Preamble []string 45 Sections []HeaderSection 46 // AllDecls maps all decls to their URL fragments. 47 AllDecls map[string]string 48} 49 50type HeaderSection struct { 51 // Preamble contains a comment for a group of functions. 52 Preamble []string 53 Decls []HeaderDecl 54 // Anchor, if non-empty, is the URL fragment to use in anchor tags. 55 Anchor string 56 // IsPrivate is true if the section contains private functions (as 57 // indicated by its name). 58 IsPrivate bool 59} 60 61type HeaderDecl struct { 62 // Comment contains a comment for a specific function. Each string is a 63 // paragraph. Some paragraph may contain \n runes to indicate that they 64 // are preformatted. 65 Comment []string 66 // Name contains the name of the function, if it could be extracted. 67 Name string 68 // Decl contains the preformatted C declaration itself. 69 Decl string 70 // Anchor, if non-empty, is the URL fragment to use in anchor tags. 71 Anchor string 72} 73 74const ( 75 cppGuard = "#if defined(__cplusplus)" 76 commentStart = "/* " 77 commentEnd = " */" 78 lineComment = "// " 79) 80 81func isComment(line string) bool { 82 return strings.HasPrefix(line, commentStart) || strings.HasPrefix(line, lineComment) 83} 84 85func commentSubject(line string) string { 86 if strings.HasPrefix(line, "A ") { 87 line = line[len("A "):] 88 } else if strings.HasPrefix(line, "An ") { 89 line = line[len("An "):] 90 } 91 idx := strings.IndexAny(line, " ,") 92 if idx < 0 { 93 return line 94 } 95 return line[:idx] 96} 97 98func extractComment(lines []string, lineNo int) (comment []string, rest []string, restLineNo int, err error) { 99 if len(lines) == 0 { 100 return nil, lines, lineNo, nil 101 } 102 103 restLineNo = lineNo 104 rest = lines 105 106 var isBlock bool 107 if strings.HasPrefix(rest[0], commentStart) { 108 isBlock = true 109 } else if !strings.HasPrefix(rest[0], lineComment) { 110 panic("extractComment called on non-comment") 111 } 112 commentParagraph := rest[0][len(commentStart):] 113 rest = rest[1:] 114 restLineNo++ 115 116 for len(rest) > 0 { 117 if isBlock { 118 i := strings.Index(commentParagraph, commentEnd) 119 if i >= 0 { 120 if i != len(commentParagraph)-len(commentEnd) { 121 err = fmt.Errorf("garbage after comment end on line %d", restLineNo) 122 return 123 } 124 commentParagraph = commentParagraph[:i] 125 if len(commentParagraph) > 0 { 126 comment = append(comment, commentParagraph) 127 } 128 return 129 } 130 } 131 132 line := rest[0] 133 if isBlock { 134 if !strings.HasPrefix(line, " *") { 135 err = fmt.Errorf("comment doesn't start with block prefix on line %d: %s", restLineNo, line) 136 return 137 } 138 } else if !strings.HasPrefix(line, "//") { 139 if len(commentParagraph) > 0 { 140 comment = append(comment, commentParagraph) 141 } 142 return 143 } 144 if len(line) == 2 || !isBlock || line[2] != '/' { 145 line = line[2:] 146 } 147 if strings.HasPrefix(line, " ") { 148 /* Identing the lines of a paragraph marks them as 149 * preformatted. */ 150 if len(commentParagraph) > 0 { 151 commentParagraph += "\n" 152 } 153 line = line[3:] 154 } 155 if len(line) > 0 { 156 commentParagraph = commentParagraph + line 157 if len(commentParagraph) > 0 && commentParagraph[0] == ' ' { 158 commentParagraph = commentParagraph[1:] 159 } 160 } else { 161 comment = append(comment, commentParagraph) 162 commentParagraph = "" 163 } 164 rest = rest[1:] 165 restLineNo++ 166 } 167 168 err = errors.New("hit EOF in comment") 169 return 170} 171 172func extractDecl(lines []string, lineNo int) (decl string, rest []string, restLineNo int, err error) { 173 if len(lines) == 0 || len(lines[0]) == 0 { 174 return "", lines, lineNo, nil 175 } 176 177 rest = lines 178 restLineNo = lineNo 179 180 var stack []rune 181 for len(rest) > 0 { 182 line := rest[0] 183 for _, c := range line { 184 switch c { 185 case '(', '{', '[': 186 stack = append(stack, c) 187 case ')', '}', ']': 188 if len(stack) == 0 { 189 err = fmt.Errorf("unexpected %c on line %d", c, restLineNo) 190 return 191 } 192 var expected rune 193 switch c { 194 case ')': 195 expected = '(' 196 case '}': 197 expected = '{' 198 case ']': 199 expected = '[' 200 default: 201 panic("internal error") 202 } 203 if last := stack[len(stack)-1]; last != expected { 204 err = fmt.Errorf("found %c when expecting %c on line %d", c, last, restLineNo) 205 return 206 } 207 stack = stack[:len(stack)-1] 208 } 209 } 210 if len(decl) > 0 { 211 decl += "\n" 212 } 213 decl += line 214 rest = rest[1:] 215 restLineNo++ 216 217 if len(stack) == 0 && (len(decl) == 0 || decl[len(decl)-1] != '\\') { 218 break 219 } 220 } 221 222 return 223} 224 225func skipLine(s string) string { 226 i := strings.Index(s, "\n") 227 if i > 0 { 228 return s[i:] 229 } 230 return "" 231} 232 233var stackOfRegexp = regexp.MustCompile(`STACK_OF\(([^)]*)\)`) 234var lhashOfRegexp = regexp.MustCompile(`LHASH_OF\(([^)]*)\)`) 235 236func getNameFromDecl(decl string) (string, bool) { 237 for strings.HasPrefix(decl, "#if") || strings.HasPrefix(decl, "#elif") { 238 decl = skipLine(decl) 239 } 240 241 if strings.HasPrefix(decl, "typedef ") { 242 return "", false 243 } 244 245 for _, prefix := range []string{"struct ", "enum ", "#define "} { 246 if !strings.HasPrefix(decl, prefix) { 247 continue 248 } 249 250 decl = strings.TrimPrefix(decl, prefix) 251 252 for len(decl) > 0 && decl[0] == ' ' { 253 decl = decl[1:] 254 } 255 256 // struct and enum types can be the return type of a 257 // function. 258 if prefix[0] != '#' && strings.Index(decl, "{") == -1 { 259 break 260 } 261 262 i := strings.IndexAny(decl, "( ") 263 if i < 0 { 264 return "", false 265 } 266 return decl[:i], true 267 } 268 decl = strings.TrimPrefix(decl, "OPENSSL_EXPORT ") 269 decl = strings.TrimPrefix(decl, "const ") 270 decl = stackOfRegexp.ReplaceAllString(decl, "STACK_OF_$1") 271 decl = lhashOfRegexp.ReplaceAllString(decl, "LHASH_OF_$1") 272 i := strings.Index(decl, "(") 273 if i < 0 { 274 return "", false 275 } 276 j := strings.LastIndex(decl[:i], " ") 277 if j < 0 { 278 return "", false 279 } 280 for j+1 < len(decl) && decl[j+1] == '*' { 281 j++ 282 } 283 return decl[j+1 : i], true 284} 285 286func sanitizeAnchor(name string) string { 287 return strings.Replace(name, " ", "-", -1) 288} 289 290func isPrivateSection(name string) bool { 291 return strings.HasPrefix(name, "Private functions") || strings.HasPrefix(name, "Private structures") || strings.Contains(name, "(hidden)") 292} 293 294func isCollectiveComment(line string) bool { 295 return strings.HasPrefix(line, "The ") || strings.HasPrefix(line, "These ") 296} 297 298func (config *Config) parseHeader(path string) (*HeaderFile, error) { 299 headerPath := filepath.Join(config.BaseDirectory, path) 300 301 headerFile, err := os.Open(headerPath) 302 if err != nil { 303 return nil, err 304 } 305 defer headerFile.Close() 306 307 scanner := bufio.NewScanner(headerFile) 308 var lines, oldLines []string 309 for scanner.Scan() { 310 lines = append(lines, scanner.Text()) 311 } 312 if err := scanner.Err(); err != nil { 313 return nil, err 314 } 315 316 lineNo := 1 317 found := false 318 for i, line := range lines { 319 if line == cppGuard { 320 lines = lines[i+1:] 321 lineNo += i + 1 322 found = true 323 break 324 } 325 } 326 327 if !found { 328 return nil, errors.New("no C++ guard found") 329 } 330 331 if len(lines) == 0 || lines[0] != "extern \"C\" {" { 332 return nil, errors.New("no extern \"C\" found after C++ guard") 333 } 334 lineNo += 2 335 lines = lines[2:] 336 337 header := &HeaderFile{ 338 Name: filepath.Base(path), 339 AllDecls: make(map[string]string), 340 } 341 342 for i, line := range lines { 343 if len(line) > 0 { 344 lines = lines[i:] 345 lineNo += i 346 break 347 } 348 } 349 350 oldLines = lines 351 if len(lines) > 0 && isComment(lines[0]) { 352 comment, rest, restLineNo, err := extractComment(lines, lineNo) 353 if err != nil { 354 return nil, err 355 } 356 357 if len(rest) > 0 && len(rest[0]) == 0 { 358 if len(rest) < 2 || len(rest[1]) != 0 { 359 return nil, errors.New("preamble comment should be followed by two blank lines") 360 } 361 header.Preamble = comment 362 lineNo = restLineNo + 2 363 lines = rest[2:] 364 } else { 365 lines = oldLines 366 } 367 } 368 369 allAnchors := make(map[string]struct{}) 370 371 for { 372 // Start of a section. 373 if len(lines) == 0 { 374 return nil, errors.New("unexpected end of file") 375 } 376 line := lines[0] 377 if line == cppGuard { 378 break 379 } 380 381 if len(line) == 0 { 382 return nil, fmt.Errorf("blank line at start of section on line %d", lineNo) 383 } 384 385 var section HeaderSection 386 387 if isComment(line) { 388 comment, rest, restLineNo, err := extractComment(lines, lineNo) 389 if err != nil { 390 return nil, err 391 } 392 if len(rest) > 0 && len(rest[0]) == 0 { 393 anchor := sanitizeAnchor(firstSentence(comment)) 394 if len(anchor) > 0 { 395 if _, ok := allAnchors[anchor]; ok { 396 return nil, fmt.Errorf("duplicate anchor: %s", anchor) 397 } 398 allAnchors[anchor] = struct{}{} 399 } 400 401 section.Preamble = comment 402 section.IsPrivate = len(comment) > 0 && isPrivateSection(comment[0]) 403 section.Anchor = anchor 404 lines = rest[1:] 405 lineNo = restLineNo + 1 406 } 407 } 408 409 for len(lines) > 0 { 410 line := lines[0] 411 if len(line) == 0 { 412 lines = lines[1:] 413 lineNo++ 414 break 415 } 416 if line == cppGuard { 417 return nil, fmt.Errorf("hit ending C++ guard while in section on line %d", lineNo) 418 } 419 420 var comment []string 421 var decl string 422 if isComment(line) { 423 comment, lines, lineNo, err = extractComment(lines, lineNo) 424 if err != nil { 425 return nil, err 426 } 427 } 428 if len(lines) == 0 { 429 return nil, fmt.Errorf("expected decl at EOF on line %d", lineNo) 430 } 431 declLineNo := lineNo 432 decl, lines, lineNo, err = extractDecl(lines, lineNo) 433 if err != nil { 434 return nil, err 435 } 436 name, ok := getNameFromDecl(decl) 437 if !ok { 438 name = "" 439 } 440 if last := len(section.Decls) - 1; len(name) == 0 && len(comment) == 0 && last >= 0 { 441 section.Decls[last].Decl += "\n" + decl 442 } else { 443 // As a matter of style, comments should start 444 // with the name of the thing that they are 445 // commenting on. We make an exception here for 446 // collective comments. 447 if len(comment) > 0 && 448 len(name) > 0 && 449 !isCollectiveComment(comment[0]) { 450 subject := commentSubject(comment[0]) 451 ok := subject == name 452 if l := len(subject); l > 0 && subject[l-1] == '*' { 453 // Groups of names, notably #defines, are often 454 // denoted with a wildcard. 455 ok = strings.HasPrefix(name, subject[:l-1]) 456 } 457 if !ok { 458 return nil, fmt.Errorf("comment for %q doesn't seem to match line %s:%d\n", name, path, declLineNo) 459 } 460 } 461 anchor := sanitizeAnchor(name) 462 // TODO(davidben): Enforce uniqueness. This is 463 // skipped because #ifdefs currently result in 464 // duplicate table-of-contents entries. 465 allAnchors[anchor] = struct{}{} 466 467 header.AllDecls[name] = anchor 468 469 section.Decls = append(section.Decls, HeaderDecl{ 470 Comment: comment, 471 Name: name, 472 Decl: decl, 473 Anchor: anchor, 474 }) 475 } 476 477 if len(lines) > 0 && len(lines[0]) == 0 { 478 lines = lines[1:] 479 lineNo++ 480 } 481 } 482 483 header.Sections = append(header.Sections, section) 484 } 485 486 return header, nil 487} 488 489func firstSentence(paragraphs []string) string { 490 if len(paragraphs) == 0 { 491 return "" 492 } 493 s := paragraphs[0] 494 i := strings.Index(s, ". ") 495 if i >= 0 { 496 return s[:i] 497 } 498 if lastIndex := len(s) - 1; s[lastIndex] == '.' { 499 return s[:lastIndex] 500 } 501 return s 502} 503 504// markupPipeWords converts |s| into an HTML string, safe to be included outside 505// a tag, while also marking up words surrounded by |. 506func markupPipeWords(allDecls map[string]string, s string, linkDecls bool) template.HTML { 507 // It is safe to look for '|' in the HTML-escaped version of |s| 508 // below. The escaped version cannot include '|' instead tags because 509 // there are no tags by construction. 510 s = template.HTMLEscapeString(s) 511 ret := "" 512 513 for { 514 i := strings.Index(s, "|") 515 if i == -1 { 516 ret += s 517 break 518 } 519 ret += s[:i] 520 s = s[i+1:] 521 522 i = strings.Index(s, "|") 523 j := strings.Index(s, " ") 524 if i > 0 && (j == -1 || j > i) { 525 ret += "<tt>" 526 anchor, isLink := allDecls[s[:i]] 527 if linkDecls && isLink { 528 ret += fmt.Sprintf("<a href=\"%s\">%s</a>", template.HTMLEscapeString(anchor), s[:i]) 529 } else { 530 ret += s[:i] 531 } 532 ret += "</tt>" 533 s = s[i+1:] 534 } else { 535 ret += "|" 536 } 537 } 538 539 return template.HTML(ret) 540} 541 542func markupFirstWord(s template.HTML) template.HTML { 543 if isCollectiveComment(string(s)) { 544 return s 545 } 546 start := 0 547again: 548 end := strings.Index(string(s[start:]), " ") 549 if end > 0 { 550 end += start 551 w := strings.ToLower(string(s[start:end])) 552 // The first word was already marked up as an HTML tag. Don't 553 // mark it up further. 554 if strings.ContainsRune(w, '<') { 555 return s 556 } 557 if w == "a" || w == "an" { 558 start = end + 1 559 goto again 560 } 561 return s[:start] + "<span class=\"first-word\">" + s[start:end] + "</span>" + s[end:] 562 } 563 return s 564} 565 566var rfcRegexp = regexp.MustCompile("RFC ([0-9]+)") 567 568func markupRFC(html template.HTML) template.HTML { 569 s := string(html) 570 matches := rfcRegexp.FindAllStringSubmatchIndex(s, -1) 571 if len(matches) == 0 { 572 return html 573 } 574 575 var b strings.Builder 576 var idx int 577 for _, match := range matches { 578 start, end := match[0], match[1] 579 number := s[match[2]:match[3]] 580 b.WriteString(s[idx:start]) 581 fmt.Fprintf(&b, "<a href=\"https://www.rfc-editor.org/rfc/rfc%s.html\">%s</a>", number, s[start:end]) 582 idx = end 583 } 584 b.WriteString(s[idx:]) 585 return template.HTML(b.String()) 586} 587 588func newlinesToBR(html template.HTML) template.HTML { 589 s := string(html) 590 if !strings.Contains(s, "\n") { 591 return html 592 } 593 s = strings.Replace(s, "\n", "<br>", -1) 594 s = strings.Replace(s, " ", " ", -1) 595 return template.HTML(s) 596} 597 598func generate(outPath string, config *Config) (map[string]string, error) { 599 allDecls := make(map[string]string) 600 601 headerTmpl := template.New("headerTmpl") 602 headerTmpl.Funcs(template.FuncMap{ 603 "firstSentence": firstSentence, 604 "markupPipeWords": func(s string) template.HTML { return markupPipeWords(allDecls, s, true /* linkDecls */) }, 605 "markupPipeWordsNoLink": func(s string) template.HTML { return markupPipeWords(allDecls, s, false /* linkDecls */) }, 606 "markupFirstWord": markupFirstWord, 607 "markupRFC": markupRFC, 608 "newlinesToBR": newlinesToBR, 609 }) 610 headerTmpl, err := headerTmpl.Parse(`<!DOCTYPE html> 611<html> 612 <head> 613 <title>BoringSSL - {{.Name}}</title> 614 <meta charset="utf-8"> 615 <link rel="stylesheet" type="text/css" href="doc.css"> 616 </head> 617 618 <body> 619 <div id="main"> 620 <div class="title"> 621 <h2>{{.Name}}</h2> 622 <a href="headers.html">All headers</a> 623 </div> 624 625 {{range .Preamble}}<p>{{. | markupPipeWords | markupRFC}}</p>{{end}} 626 627 <ol> 628 {{range .Sections}} 629 {{if not .IsPrivate}} 630 {{if .Anchor}}<li class="header"><a href="#{{.Anchor}}">{{.Preamble | firstSentence | markupPipeWordsNoLink}}</a></li>{{end}} 631 {{range .Decls}} 632 {{if .Anchor}}<li><a href="#{{.Anchor}}"><tt>{{.Name}}</tt></a></li>{{end}} 633 {{end}} 634 {{end}} 635 {{end}} 636 </ol> 637 638 {{range .Sections}} 639 {{if not .IsPrivate}} 640 <div class="section" {{if .Anchor}}id="{{.Anchor}}"{{end}}> 641 {{if .Preamble}} 642 <div class="sectionpreamble"> 643 {{range .Preamble}}<p>{{. | markupPipeWords | markupRFC}}</p>{{end}} 644 </div> 645 {{end}} 646 647 {{range .Decls}} 648 <div class="decl" {{if .Anchor}}id="{{.Anchor}}"{{end}}> 649 {{range .Comment}} 650 <p>{{. | markupPipeWords | newlinesToBR | markupFirstWord | markupRFC}}</p> 651 {{end}} 652 <pre>{{.Decl}}</pre> 653 </div> 654 {{end}} 655 </div> 656 {{end}} 657 {{end}} 658 </div> 659 </body> 660</html>`) 661 if err != nil { 662 return nil, err 663 } 664 665 headerDescriptions := make(map[string]string) 666 var headers []*HeaderFile 667 668 for _, section := range config.Sections { 669 for _, headerPath := range section.Headers { 670 header, err := config.parseHeader(headerPath) 671 if err != nil { 672 return nil, errors.New("while parsing " + headerPath + ": " + err.Error()) 673 } 674 headerDescriptions[header.Name] = firstSentence(header.Preamble) 675 headers = append(headers, header) 676 677 for name, anchor := range header.AllDecls { 678 allDecls[name] = fmt.Sprintf("%s#%s", header.Name+".html", anchor) 679 } 680 } 681 } 682 683 for _, header := range headers { 684 filename := filepath.Join(outPath, header.Name+".html") 685 file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) 686 if err != nil { 687 panic(err) 688 } 689 defer file.Close() 690 if err := headerTmpl.Execute(file, header); err != nil { 691 return nil, err 692 } 693 } 694 695 return headerDescriptions, nil 696} 697 698func generateIndex(outPath string, config *Config, headerDescriptions map[string]string) error { 699 indexTmpl := template.New("indexTmpl") 700 indexTmpl.Funcs(template.FuncMap{ 701 "baseName": filepath.Base, 702 "headerDescription": func(header string) string { 703 return headerDescriptions[header] 704 }, 705 }) 706 indexTmpl, err := indexTmpl.Parse(`<!DOCTYPE html5> 707 708 <head> 709 <title>BoringSSL - Headers</title> 710 <meta charset="utf-8"> 711 <link rel="stylesheet" type="text/css" href="doc.css"> 712 </head> 713 714 <body> 715 <div id="main"> 716 <div class="title"> 717 <h2>BoringSSL Headers</h2> 718 </div> 719 <table> 720 {{range .Sections}} 721 <tr class="header"><td colspan="2">{{.Name}}</td></tr> 722 {{range .Headers}} 723 <tr><td><a href="{{. | baseName}}.html">{{. | baseName}}</a></td><td>{{. | baseName | headerDescription}}</td></tr> 724 {{end}} 725 {{end}} 726 </table> 727 </div> 728 </body> 729</html>`) 730 731 if err != nil { 732 return err 733 } 734 735 file, err := os.OpenFile(filepath.Join(outPath, "headers.html"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) 736 if err != nil { 737 panic(err) 738 } 739 defer file.Close() 740 741 if err := indexTmpl.Execute(file, config); err != nil { 742 return err 743 } 744 745 return nil 746} 747 748func copyFile(outPath string, inFilePath string) error { 749 bytes, err := ioutil.ReadFile(inFilePath) 750 if err != nil { 751 return err 752 } 753 return ioutil.WriteFile(filepath.Join(outPath, filepath.Base(inFilePath)), bytes, 0666) 754} 755 756func main() { 757 var ( 758 configFlag *string = flag.String("config", "doc.config", "Location of config file") 759 outputDir *string = flag.String("out", ".", "Path to the directory where the output will be written") 760 config Config 761 ) 762 763 flag.Parse() 764 765 if len(*configFlag) == 0 { 766 fmt.Printf("No config file given by --config\n") 767 os.Exit(1) 768 } 769 770 if len(*outputDir) == 0 { 771 fmt.Printf("No output directory given by --out\n") 772 os.Exit(1) 773 } 774 775 configBytes, err := ioutil.ReadFile(*configFlag) 776 if err != nil { 777 fmt.Printf("Failed to open config file: %s\n", err) 778 os.Exit(1) 779 } 780 781 if err := json.Unmarshal(configBytes, &config); err != nil { 782 fmt.Printf("Failed to parse config file: %s\n", err) 783 os.Exit(1) 784 } 785 786 headerDescriptions, err := generate(*outputDir, &config) 787 if err != nil { 788 fmt.Printf("Failed to generate output: %s\n", err) 789 os.Exit(1) 790 } 791 792 if err := generateIndex(*outputDir, &config, headerDescriptions); err != nil { 793 fmt.Printf("Failed to generate index: %s\n", err) 794 os.Exit(1) 795 } 796 797 if err := copyFile(*outputDir, "doc.css"); err != nil { 798 fmt.Printf("Failed to copy static file: %s\n", err) 799 os.Exit(1) 800 } 801} 802