1//go:build ignore 2 3// doc generates HTML files from the comments in header files. 4// 5// doc expects to be given the path to a JSON file via the --config option. 6// From that JSON (which is defined by the Config struct) it reads a list of 7// header file locations and generates HTML files for each in the current 8// directory. 9package main 10 11import ( 12 "bufio" 13 "encoding/json" 14 "errors" 15 "flag" 16 "fmt" 17 "html/template" 18 "os" 19 "path/filepath" 20 "regexp" 21 "strconv" 22 "strings" 23 "unicode" 24) 25 26// Config describes the structure of the config JSON file. 27type Config struct { 28 // BaseDirectory is a path to which other paths in the file are 29 // relative. 30 BaseDirectory string 31 Sections []ConfigSection 32} 33 34type ConfigSection struct { 35 Name string 36 // Headers is a list of paths to header files. 37 Headers []string 38} 39 40// HeaderFile is the internal representation of a header file. 41type HeaderFile struct { 42 // Name is the basename of the header file (e.g. "ex_data.html"). 43 Name string 44 // Preamble contains a comment for the file as a whole. Each string 45 // is a separate paragraph. 46 Preamble []CommentBlock 47 Sections []HeaderSection 48 // AllDecls maps all decls to their URL fragments. 49 AllDecls map[string]string 50} 51 52type HeaderSection struct { 53 // Preamble contains a comment for a group of functions. 54 Preamble []CommentBlock 55 Decls []HeaderDecl 56 // Anchor, if non-empty, is the URL fragment to use in anchor tags. 57 Anchor string 58 // IsPrivate is true if the section contains private functions (as 59 // indicated by its name). 60 IsPrivate bool 61} 62 63type HeaderDecl struct { 64 // Comment contains a comment for a specific function. Each string is a 65 // paragraph. Some paragraph may contain \n runes to indicate that they 66 // are preformatted. 67 Comment []CommentBlock 68 // Name contains the name of the function, if it could be extracted. 69 Name string 70 // Decl contains the preformatted C declaration itself. 71 Decl string 72 // Anchor, if non-empty, is the URL fragment to use in anchor tags. 73 Anchor string 74} 75 76type CommentBlockType int 77 78const ( 79 CommentParagraph CommentBlockType = iota 80 CommentOrderedListItem 81 CommentBulletListItem 82 CommentCode 83) 84 85type CommentBlock struct { 86 Type CommentBlockType 87 Paragraph string 88} 89 90const ( 91 cppGuard = "#if defined(__cplusplus)" 92 commentStart = "/* " 93 commentEnd = " */" 94 lineComment = "// " 95) 96 97func isComment(line string) bool { 98 return strings.HasPrefix(line, commentStart) || strings.HasPrefix(line, lineComment) 99} 100 101func commentSubject(line string) string { 102 if strings.HasPrefix(line, "A ") { 103 line = line[len("A "):] 104 } else if strings.HasPrefix(line, "An ") { 105 line = line[len("An "):] 106 } 107 idx := strings.IndexAny(line, " ,") 108 if idx < 0 { 109 return line 110 } 111 return line[:idx] 112} 113 114func extractCommentLines(lines []string, lineNo int) (comment []string, rest []string, restLineNo int, err error) { 115 if len(lines) == 0 { 116 return nil, lines, lineNo, nil 117 } 118 119 restLineNo = lineNo 120 rest = lines 121 122 var isBlock bool 123 if strings.HasPrefix(rest[0], commentStart) { 124 isBlock = true 125 } else if !strings.HasPrefix(rest[0], lineComment) { 126 panic("extractComment called on non-comment") 127 } 128 comment = []string{rest[0][len(commentStart):]} 129 rest = rest[1:] 130 restLineNo++ 131 132 for len(rest) > 0 { 133 if isBlock { 134 last := &comment[len(comment)-1] 135 if i := strings.Index(*last, commentEnd); i >= 0 { 136 if i != len(*last)-len(commentEnd) { 137 err = fmt.Errorf("garbage after comment end on line %d", restLineNo) 138 return 139 } 140 *last = (*last)[:i] 141 return 142 } 143 } 144 145 line := rest[0] 146 if isBlock { 147 if !strings.HasPrefix(line, " *") { 148 err = fmt.Errorf("comment doesn't start with block prefix on line %d: %s", restLineNo, line) 149 return 150 } 151 } else if !strings.HasPrefix(line, "//") { 152 return 153 } 154 comment = append(comment, line[2:]) 155 rest = rest[1:] 156 restLineNo++ 157 } 158 159 err = errors.New("hit EOF in comment") 160 return 161} 162 163func removeBulletListMarker(line string) (string, bool) { 164 orig := line 165 line = strings.TrimSpace(line) 166 if !strings.HasPrefix(line, "+ ") && !strings.HasPrefix(line, "- ") && !strings.HasPrefix(line, "* ") { 167 return orig, false 168 } 169 return line[2:], true 170} 171 172func removeOrderedListMarker(line string) (rest string, num int, ok bool) { 173 orig := line 174 line = strings.TrimSpace(line) 175 if len(line) == 0 || !unicode.IsDigit(rune(line[0])) { 176 return orig, -1, false 177 } 178 179 l := 0 180 for l < len(line) && unicode.IsDigit(rune(line[l])) { 181 l++ 182 } 183 num, err := strconv.Atoi(line[:l]) 184 if err != nil { 185 return orig, -1, false 186 } 187 188 line = line[l:] 189 if line, ok := strings.CutPrefix(line, ". "); ok { 190 return line, num, true 191 } 192 if line, ok := strings.CutPrefix(line, ") "); ok { 193 return line, num, true 194 } 195 196 return orig, -1, false 197} 198 199func removeCodeIndent(line string) (string, bool) { 200 return strings.CutPrefix(line, " ") 201} 202 203func extractComment(lines []string, lineNo int) (comment []CommentBlock, rest []string, restLineNo int, err error) { 204 commentLines, rest, restLineNo, err := extractCommentLines(lines, lineNo) 205 if err != nil { 206 return 207 } 208 209 // This syntax and parsing algorithm is loosely inspired by CommonMark, 210 // but reduced to a small subset with no nesting. Blocks being open vs. 211 // closed can be tracked implicitly. We're also much slopplier about how 212 // indentation. Additionally, rather than grouping list items into 213 // lists, our parser just emits a list items, which are grouped later at 214 // rendering time. 215 // 216 // If we later need more features, such as nested lists, this can evolve 217 // into a more complex implementation. 218 var numBlankLines int 219 for _, line := range commentLines { 220 // Defer blank lines until we know the next element. 221 if len(strings.TrimSpace(line)) == 0 { 222 numBlankLines++ 223 continue 224 } 225 226 blankLinesSkipped := numBlankLines 227 numBlankLines = 0 228 229 // Attempt to continue the previous block. 230 if len(comment) > 0 { 231 last := &comment[len(comment)-1] 232 if last.Type == CommentCode { 233 l, ok := removeCodeIndent(line) 234 if ok { 235 for i := 0; i < blankLinesSkipped; i++ { 236 last.Paragraph += "\n" 237 } 238 last.Paragraph += l + "\n" 239 continue 240 } 241 } else if blankLinesSkipped == 0 { 242 _, isBulletList := removeBulletListMarker(line) 243 _, num, isOrderedList := removeOrderedListMarker(line) 244 if isOrderedList && last.Type == CommentParagraph && num != 1 { 245 // A list item can only interrupt a paragraph if the number is one. 246 // See the discussion in https://spec.commonmark.org/0.30/#lists. 247 // This avoids wrapping like "(See RFC\n5280)" turning into a list. 248 isOrderedList = false 249 } 250 if !isBulletList && !isOrderedList { 251 // This is a continuation line of the previous paragraph. 252 last.Paragraph += " " + strings.TrimSpace(line) 253 continue 254 } 255 } 256 } 257 258 // Make a new block. 259 if line, ok := removeBulletListMarker(line); ok { 260 comment = append(comment, CommentBlock{ 261 Type: CommentBulletListItem, 262 Paragraph: strings.TrimSpace(line), 263 }) 264 } else if line, _, ok := removeOrderedListMarker(line); ok { 265 comment = append(comment, CommentBlock{ 266 Type: CommentOrderedListItem, 267 Paragraph: strings.TrimSpace(line), 268 }) 269 } else if line, ok := removeCodeIndent(line); ok { 270 comment = append(comment, CommentBlock{ 271 Type: CommentCode, 272 Paragraph: line + "\n", 273 }) 274 } else { 275 comment = append(comment, CommentBlock{ 276 Type: CommentParagraph, 277 Paragraph: strings.TrimSpace(line), 278 }) 279 } 280 } 281 282 return 283} 284 285func extractDecl(lines []string, lineNo int) (decl string, rest []string, restLineNo int, err error) { 286 if len(lines) == 0 || len(lines[0]) == 0 { 287 return "", lines, lineNo, nil 288 } 289 290 rest = lines 291 restLineNo = lineNo 292 293 var stack []rune 294 for len(rest) > 0 { 295 line := rest[0] 296 for _, c := range line { 297 switch c { 298 case '(', '{', '[': 299 stack = append(stack, c) 300 case ')', '}', ']': 301 if len(stack) == 0 { 302 err = fmt.Errorf("unexpected %c on line %d", c, restLineNo) 303 return 304 } 305 var expected rune 306 switch c { 307 case ')': 308 expected = '(' 309 case '}': 310 expected = '{' 311 case ']': 312 expected = '[' 313 default: 314 panic("internal error") 315 } 316 if last := stack[len(stack)-1]; last != expected { 317 err = fmt.Errorf("found %c when expecting %c on line %d", c, last, restLineNo) 318 return 319 } 320 stack = stack[:len(stack)-1] 321 } 322 } 323 if len(decl) > 0 { 324 decl += "\n" 325 } 326 decl += line 327 rest = rest[1:] 328 restLineNo++ 329 330 if len(stack) == 0 && (len(decl) == 0 || decl[len(decl)-1] != '\\') { 331 break 332 } 333 } 334 335 return 336} 337 338func skipLine(s string) string { 339 i := strings.Index(s, "\n") 340 if i > 0 { 341 return s[i:] 342 } 343 return "" 344} 345 346var stackOfRegexp = regexp.MustCompile(`STACK_OF\(([^)]*)\)`) 347var lhashOfRegexp = regexp.MustCompile(`LHASH_OF\(([^)]*)\)`) 348 349func getNameFromDecl(decl string) (string, bool) { 350 for strings.HasPrefix(decl, "#if") || strings.HasPrefix(decl, "#elif") { 351 decl = skipLine(decl) 352 } 353 354 if strings.HasPrefix(decl, "typedef ") { 355 return "", false 356 } 357 358 for _, prefix := range []string{"struct ", "enum ", "#define "} { 359 if !strings.HasPrefix(decl, prefix) { 360 continue 361 } 362 363 decl = strings.TrimPrefix(decl, prefix) 364 365 for len(decl) > 0 && decl[0] == ' ' { 366 decl = decl[1:] 367 } 368 369 // struct and enum types can be the return type of a 370 // function. 371 if prefix[0] != '#' && strings.Index(decl, "{") == -1 { 372 break 373 } 374 375 i := strings.IndexAny(decl, "( ") 376 if i < 0 { 377 return "", false 378 } 379 return decl[:i], true 380 } 381 decl = strings.TrimPrefix(decl, "OPENSSL_EXPORT ") 382 decl = strings.TrimPrefix(decl, "const ") 383 decl = stackOfRegexp.ReplaceAllString(decl, "STACK_OF_$1") 384 decl = lhashOfRegexp.ReplaceAllString(decl, "LHASH_OF_$1") 385 i := strings.Index(decl, "(") 386 if i < 0 { 387 return "", false 388 } 389 j := strings.LastIndex(decl[:i], " ") 390 if j < 0 { 391 return "", false 392 } 393 for j+1 < len(decl) && decl[j+1] == '*' { 394 j++ 395 } 396 return decl[j+1 : i], true 397} 398 399func sanitizeAnchor(name string) string { 400 return strings.Replace(name, " ", "-", -1) 401} 402 403func isPrivateSection(name string) bool { 404 return strings.HasPrefix(name, "Private functions") || strings.HasPrefix(name, "Private structures") || strings.Contains(name, "(hidden)") 405} 406 407func isCollectiveComment(line string) bool { 408 return strings.HasPrefix(line, "The ") || strings.HasPrefix(line, "These ") 409} 410 411func (config *Config) parseHeader(path string) (*HeaderFile, error) { 412 headerPath := filepath.Join(config.BaseDirectory, path) 413 414 headerFile, err := os.Open(headerPath) 415 if err != nil { 416 return nil, err 417 } 418 defer headerFile.Close() 419 420 scanner := bufio.NewScanner(headerFile) 421 var lines, oldLines []string 422 for scanner.Scan() { 423 lines = append(lines, scanner.Text()) 424 } 425 if err := scanner.Err(); err != nil { 426 return nil, err 427 } 428 429 lineNo := 1 430 found := false 431 for i, line := range lines { 432 if line == cppGuard { 433 lines = lines[i+1:] 434 lineNo += i + 1 435 found = true 436 break 437 } 438 } 439 440 if !found { 441 return nil, errors.New("no C++ guard found") 442 } 443 444 if len(lines) == 0 || lines[0] != "extern \"C\" {" { 445 return nil, errors.New("no extern \"C\" found after C++ guard") 446 } 447 lineNo += 2 448 lines = lines[2:] 449 450 header := &HeaderFile{ 451 Name: filepath.Base(path), 452 AllDecls: make(map[string]string), 453 } 454 455 for i, line := range lines { 456 if len(line) > 0 { 457 lines = lines[i:] 458 lineNo += i 459 break 460 } 461 } 462 463 oldLines = lines 464 if len(lines) > 0 && isComment(lines[0]) { 465 comment, rest, restLineNo, err := extractComment(lines, lineNo) 466 if err != nil { 467 return nil, err 468 } 469 470 if len(rest) > 0 && len(rest[0]) == 0 { 471 if len(rest) < 2 || len(rest[1]) != 0 { 472 return nil, errors.New("preamble comment should be followed by two blank lines") 473 } 474 header.Preamble = comment 475 lineNo = restLineNo + 2 476 lines = rest[2:] 477 } else { 478 lines = oldLines 479 } 480 } 481 482 allAnchors := make(map[string]struct{}) 483 484 for { 485 // Start of a section. 486 if len(lines) == 0 { 487 return nil, errors.New("unexpected end of file") 488 } 489 line := lines[0] 490 if line == cppGuard { 491 break 492 } 493 494 if len(line) == 0 { 495 return nil, fmt.Errorf("blank line at start of section on line %d", lineNo) 496 } 497 498 var section HeaderSection 499 500 if isComment(line) { 501 comment, rest, restLineNo, err := extractComment(lines, lineNo) 502 if err != nil { 503 return nil, err 504 } 505 if len(rest) > 0 && len(rest[0]) == 0 { 506 heading := firstSentence(comment) 507 anchor := sanitizeAnchor(heading) 508 if len(anchor) > 0 { 509 if _, ok := allAnchors[anchor]; ok { 510 return nil, fmt.Errorf("duplicate anchor: %s", anchor) 511 } 512 allAnchors[anchor] = struct{}{} 513 } 514 515 section.Preamble = comment 516 section.IsPrivate = isPrivateSection(heading) 517 section.Anchor = anchor 518 lines = rest[1:] 519 lineNo = restLineNo + 1 520 } 521 } 522 523 for len(lines) > 0 { 524 line := lines[0] 525 if len(line) == 0 { 526 lines = lines[1:] 527 lineNo++ 528 break 529 } 530 if line == cppGuard { 531 return nil, fmt.Errorf("hit ending C++ guard while in section on line %d (possibly missing two empty lines ahead of guard?)", lineNo) 532 } 533 534 var comment []CommentBlock 535 var decl string 536 if isComment(line) { 537 comment, lines, lineNo, err = extractComment(lines, lineNo) 538 if err != nil { 539 return nil, err 540 } 541 } 542 if len(lines) == 0 { 543 return nil, fmt.Errorf("expected decl at EOF on line %d", lineNo) 544 } 545 declLineNo := lineNo 546 decl, lines, lineNo, err = extractDecl(lines, lineNo) 547 if err != nil { 548 return nil, err 549 } 550 name, ok := getNameFromDecl(decl) 551 if !ok { 552 name = "" 553 } 554 if last := len(section.Decls) - 1; len(name) == 0 && len(comment) == 0 && last >= 0 { 555 section.Decls[last].Decl += "\n" + decl 556 } else { 557 // As a matter of style, comments should start 558 // with the name of the thing that they are 559 // commenting on. We make an exception here for 560 // collective comments. 561 sentence := firstSentence(comment) 562 if len(comment) > 0 && 563 len(name) > 0 && 564 !isCollectiveComment(sentence) { 565 subject := commentSubject(sentence) 566 ok := subject == name 567 if l := len(subject); l > 0 && subject[l-1] == '*' { 568 // Groups of names, notably #defines, are often 569 // denoted with a wildcard. 570 ok = strings.HasPrefix(name, subject[:l-1]) 571 } 572 if !ok { 573 return nil, fmt.Errorf("comment for %q doesn't seem to match line %s:%d\n", name, path, declLineNo) 574 } 575 } 576 anchor := sanitizeAnchor(name) 577 // TODO(davidben): Enforce uniqueness. This is 578 // skipped because #ifdefs currently result in 579 // duplicate table-of-contents entries. 580 allAnchors[anchor] = struct{}{} 581 582 header.AllDecls[name] = anchor 583 584 section.Decls = append(section.Decls, HeaderDecl{ 585 Comment: comment, 586 Name: name, 587 Decl: decl, 588 Anchor: anchor, 589 }) 590 } 591 592 if len(lines) > 0 && len(lines[0]) == 0 { 593 lines = lines[1:] 594 lineNo++ 595 } 596 } 597 598 header.Sections = append(header.Sections, section) 599 } 600 601 return header, nil 602} 603 604func firstSentence(comment []CommentBlock) string { 605 if len(comment) == 0 { 606 return "" 607 } 608 s := comment[0].Paragraph 609 i := strings.Index(s, ". ") 610 if i >= 0 { 611 return s[:i] 612 } 613 if lastIndex := len(s) - 1; s[lastIndex] == '.' { 614 return s[:lastIndex] 615 } 616 return s 617} 618 619func markupComment(allDecls map[string]string, comment []CommentBlock) template.HTML { 620 var b strings.Builder 621 lastType := CommentParagraph 622 closeList := func() { 623 if lastType == CommentOrderedListItem { 624 b.WriteString("</ol>") 625 } else if lastType == CommentBulletListItem { 626 b.WriteString("</ul>") 627 } 628 } 629 630 for _, block := range comment { 631 // Group consecutive list items of the same type into a list. 632 if block.Type != lastType { 633 closeList() 634 if block.Type == CommentOrderedListItem { 635 b.WriteString("<ol>") 636 } else if block.Type == CommentBulletListItem { 637 b.WriteString("<ul>") 638 } 639 } 640 lastType = block.Type 641 642 switch block.Type { 643 case CommentParagraph: 644 if strings.HasPrefix(block.Paragraph, "WARNING:") { 645 b.WriteString("<p class=\"warning\">") 646 } else { 647 b.WriteString("<p>") 648 } 649 b.WriteString(string(markupParagraph(allDecls, block.Paragraph))) 650 b.WriteString("</p>") 651 case CommentOrderedListItem, CommentBulletListItem: 652 b.WriteString("<li>") 653 b.WriteString(string(markupParagraph(allDecls, block.Paragraph))) 654 b.WriteString("</li>") 655 case CommentCode: 656 b.WriteString("<pre>") 657 b.WriteString(block.Paragraph) 658 b.WriteString("</pre>") 659 default: 660 panic(block.Type) 661 } 662 } 663 664 closeList() 665 return template.HTML(b.String()) 666} 667 668func markupParagraph(allDecls map[string]string, s string) template.HTML { 669 // TODO(davidben): Ideally the inline transforms would be unified into 670 // one pass, so that the HTML output of one pass does not interfere with 671 // the next. 672 ret := markupPipeWords(allDecls, s, true /* linkDecls */) 673 ret = markupFirstWord(ret) 674 ret = markupRFC(ret) 675 return ret 676} 677 678// markupPipeWords converts |s| into an HTML string, safe to be included outside 679// a tag, while also marking up words surrounded by |. 680func markupPipeWords(allDecls map[string]string, s string, linkDecls bool) template.HTML { 681 // It is safe to look for '|' in the HTML-escaped version of |s| 682 // below. The escaped version cannot include '|' instead tags because 683 // there are no tags by construction. 684 s = template.HTMLEscapeString(s) 685 ret := "" 686 687 for { 688 i := strings.Index(s, "|") 689 if i == -1 { 690 ret += s 691 break 692 } 693 ret += s[:i] 694 s = s[i+1:] 695 696 i = strings.Index(s, "|") 697 j := strings.Index(s, " ") 698 if i > 0 && (j == -1 || j > i) { 699 ret += "<tt>" 700 anchor, isLink := allDecls[s[:i]] 701 if linkDecls && isLink { 702 ret += fmt.Sprintf("<a href=\"%s\">%s</a>", template.HTMLEscapeString(anchor), s[:i]) 703 } else { 704 ret += s[:i] 705 } 706 ret += "</tt>" 707 s = s[i+1:] 708 } else { 709 ret += "|" 710 } 711 } 712 713 return template.HTML(ret) 714} 715 716func markupFirstWord(s template.HTML) template.HTML { 717 if isCollectiveComment(string(s)) { 718 return s 719 } 720 start := 0 721again: 722 end := strings.Index(string(s[start:]), " ") 723 if end > 0 { 724 end += start 725 w := strings.ToLower(string(s[start:end])) 726 // The first word was already marked up as an HTML tag. Don't 727 // mark it up further. 728 if strings.ContainsRune(w, '<') { 729 return s 730 } 731 if w == "a" || w == "an" { 732 start = end + 1 733 goto again 734 } 735 return s[:start] + "<span class=\"first-word\">" + s[start:end] + "</span>" + s[end:] 736 } 737 return s 738} 739 740var rfcRegexp = regexp.MustCompile("RFC ([0-9]+)") 741 742func markupRFC(html template.HTML) template.HTML { 743 s := string(html) 744 matches := rfcRegexp.FindAllStringSubmatchIndex(s, -1) 745 if len(matches) == 0 { 746 return html 747 } 748 749 var b strings.Builder 750 var idx int 751 for _, match := range matches { 752 start, end := match[0], match[1] 753 number := s[match[2]:match[3]] 754 b.WriteString(s[idx:start]) 755 fmt.Fprintf(&b, "<a href=\"https://www.rfc-editor.org/rfc/rfc%s.html\">%s</a>", number, s[start:end]) 756 idx = end 757 } 758 b.WriteString(s[idx:]) 759 return template.HTML(b.String()) 760} 761 762func generate(outPath string, config *Config) (map[string]string, error) { 763 allDecls := make(map[string]string) 764 765 headerTmpl := template.New("headerTmpl") 766 headerTmpl.Funcs(template.FuncMap{ 767 "firstSentence": firstSentence, 768 "markupPipeWordsNoLink": func(s string) template.HTML { return markupPipeWords(allDecls, s, false /* linkDecls */) }, 769 "markupComment": func(c []CommentBlock) template.HTML { return markupComment(allDecls, c) }, 770 }) 771 headerTmpl, err := headerTmpl.Parse(`<!DOCTYPE html> 772<html> 773 <head> 774 <title>BoringSSL - {{.Name}}</title> 775 <meta charset="utf-8"> 776 <link rel="stylesheet" type="text/css" href="doc.css"> 777 </head> 778 779 <body> 780 <div id="main"> 781 <div class="title"> 782 <h2>{{.Name}}</h2> 783 <a href="headers.html">All headers</a> 784 </div> 785 786 {{if .Preamble}}<div class="comment">{{.Preamble | markupComment}}</div>{{end}} 787 788 <ol class="toc"> 789 {{range .Sections}} 790 {{if not .IsPrivate}} 791 {{if .Anchor}}<li class="header"><a href="#{{.Anchor}}">{{.Preamble | firstSentence | markupPipeWordsNoLink}}</a></li>{{end}} 792 {{range .Decls}} 793 {{if .Anchor}}<li><a href="#{{.Anchor}}"><tt>{{.Name}}</tt></a></li>{{end}} 794 {{end}} 795 {{end}} 796 {{end}} 797 </ol> 798 799 {{range .Sections}} 800 {{if not .IsPrivate}} 801 <div class="section" {{if .Anchor}}id="{{.Anchor}}"{{end}}> 802 {{if .Preamble}}<div class="sectionpreamble comment">{{.Preamble | markupComment}}</div>{{end}} 803 804 {{range .Decls}} 805 <div class="decl" {{if .Anchor}}id="{{.Anchor}}"{{end}}> 806 {{if .Comment}}<div class="comment">{{.Comment | markupComment}}</div>{{end}} 807 {{if .Decl}}<pre class="code">{{.Decl}}</pre>{{end}} 808 </div> 809 {{end}} 810 </div> 811 {{end}} 812 {{end}} 813 </div> 814 </body> 815</html>`) 816 if err != nil { 817 return nil, err 818 } 819 820 headerDescriptions := make(map[string]string) 821 var headers []*HeaderFile 822 823 for _, section := range config.Sections { 824 for _, headerPath := range section.Headers { 825 header, err := config.parseHeader(headerPath) 826 if err != nil { 827 return nil, errors.New("while parsing " + headerPath + ": " + err.Error()) 828 } 829 headerDescriptions[header.Name] = firstSentence(header.Preamble) 830 headers = append(headers, header) 831 832 for name, anchor := range header.AllDecls { 833 allDecls[name] = fmt.Sprintf("%s#%s", header.Name+".html", anchor) 834 } 835 } 836 } 837 838 for _, header := range headers { 839 filename := filepath.Join(outPath, header.Name+".html") 840 file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) 841 if err != nil { 842 panic(err) 843 } 844 defer file.Close() 845 if err := headerTmpl.Execute(file, header); err != nil { 846 return nil, err 847 } 848 } 849 850 return headerDescriptions, nil 851} 852 853func generateIndex(outPath string, config *Config, headerDescriptions map[string]string) error { 854 indexTmpl := template.New("indexTmpl") 855 indexTmpl.Funcs(template.FuncMap{ 856 "baseName": filepath.Base, 857 "headerDescription": func(header string) string { 858 return headerDescriptions[header] 859 }, 860 }) 861 indexTmpl, err := indexTmpl.Parse(`<!DOCTYPE html5> 862 863 <head> 864 <title>BoringSSL - Headers</title> 865 <meta charset="utf-8"> 866 <link rel="stylesheet" type="text/css" href="doc.css"> 867 </head> 868 869 <body> 870 <div id="main"> 871 <div class="title"> 872 <h2>BoringSSL Headers</h2> 873 </div> 874 <table> 875 {{range .Sections}} 876 <tr class="header"><td colspan="2">{{.Name}}</td></tr> 877 {{range .Headers}} 878 <tr><td><a href="{{. | baseName}}.html">{{. | baseName}}</a></td><td>{{. | baseName | headerDescription}}</td></tr> 879 {{end}} 880 {{end}} 881 </table> 882 </div> 883 </body> 884</html>`) 885 886 if err != nil { 887 return err 888 } 889 890 file, err := os.OpenFile(filepath.Join(outPath, "headers.html"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) 891 if err != nil { 892 panic(err) 893 } 894 defer file.Close() 895 896 if err := indexTmpl.Execute(file, config); err != nil { 897 return err 898 } 899 900 return nil 901} 902 903func copyFile(outPath string, inFilePath string) error { 904 bytes, err := os.ReadFile(inFilePath) 905 if err != nil { 906 return err 907 } 908 return os.WriteFile(filepath.Join(outPath, filepath.Base(inFilePath)), bytes, 0666) 909} 910 911func main() { 912 var ( 913 configFlag *string = flag.String("config", "doc.config", "Location of config file") 914 outputDir *string = flag.String("out", ".", "Path to the directory where the output will be written") 915 config Config 916 ) 917 918 flag.Parse() 919 920 if len(*configFlag) == 0 { 921 fmt.Printf("No config file given by --config\n") 922 os.Exit(1) 923 } 924 925 if len(*outputDir) == 0 { 926 fmt.Printf("No output directory given by --out\n") 927 os.Exit(1) 928 } 929 930 configBytes, err := os.ReadFile(*configFlag) 931 if err != nil { 932 fmt.Printf("Failed to open config file: %s\n", err) 933 os.Exit(1) 934 } 935 936 if err := json.Unmarshal(configBytes, &config); err != nil { 937 fmt.Printf("Failed to parse config file: %s\n", err) 938 os.Exit(1) 939 } 940 941 headerDescriptions, err := generate(*outputDir, &config) 942 if err != nil { 943 fmt.Printf("Failed to generate output: %s\n", err) 944 os.Exit(1) 945 } 946 947 if err := generateIndex(*outputDir, &config, headerDescriptions); err != nil { 948 fmt.Printf("Failed to generate index: %s\n", err) 949 os.Exit(1) 950 } 951 952 if err := copyFile(*outputDir, "doc.css"); err != nil { 953 fmt.Printf("Failed to copy static file: %s\n", err) 954 os.Exit(1) 955 } 956} 957