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.
 
 
 
 
 
 

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