You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

757 lines
18 KiB

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