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.
 
 
 
 
 
 

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