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 "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 if len(line) == 2 || line[2] != '/' { 115 line = line[2:] 116 } 117 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 { 198 return s[i:] 199 } 200 return s 201} 202 203func skipLine(s string) string { 204 i := strings.Index(s, "\n") 205 if i > 0 { 206 return s[i:] 207 } 208 return "" 209} 210 211func getNameFromDecl(decl string) (string, bool) { 212 for strings.HasPrefix(decl, "#if") || strings.HasPrefix(decl, "#elif") { 213 decl = skipLine(decl) 214 } 215 if strings.HasPrefix(decl, "struct ") { 216 return "", false 217 } 218 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 } 230 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 { 448 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:] 459 } 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, " ", " ", -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 }) 481 headerTmpl, err := headerTmpl.Parse(`<!DOCTYPE html> 482<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}} 498 {{if .Preamble}}<li class="header"><a href="#section-{{.Num}}">{{.Preamble | firstSentence | html | markupPipeWords}}</a></li>{{end}} 499 {{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 608func main() { 609 var ( 610 configFlag *string = flag.String("config", "doc.config", "Location of config file") 611 outputDir *string = flag.String("out", ".", "Path to the directory where the output will be written") 612 config Config 613 ) 614 615 flag.Parse() 616 617 if len(*configFlag) == 0 { 618 fmt.Printf("No config file given by --config\n") 619 os.Exit(1) 620 } 621 622 if len(*outputDir) == 0 { 623 fmt.Printf("No output directory given by --out\n") 624 os.Exit(1) 625 } 626 627 configBytes, err := ioutil.ReadFile(*configFlag) 628 if err != nil { 629 fmt.Printf("Failed to open config file: %s\n", err) 630 os.Exit(1) 631 } 632 633 if err := json.Unmarshal(configBytes, &config); err != nil { 634 fmt.Printf("Failed to parse config file: %s\n", err) 635 os.Exit(1) 636 } 637 638 headerDescriptions, err := generate(*outputDir, &config) 639 if err != nil { 640 fmt.Printf("Failed to generate output: %s\n", err) 641 os.Exit(1) 642 } 643 644 if err := generateIndex(*outputDir, &config, headerDescriptions); err != nil { 645 fmt.Printf("Failed to generate index: %s\n", err) 646 os.Exit(1) 647 } 648} 649