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