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.
 
 
 
 
 
 

616 rivejä
14 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. }
  41. type HeaderSection struct {
  42. // Preamble contains a comment for a group of functions.
  43. Preamble []string
  44. Decls []HeaderDecl
  45. // Num is just the index of the section. It's included in order to help
  46. // text/template generate anchors.
  47. Num int
  48. // IsPrivate is true if the section contains private functions (as
  49. // indicated by its name).
  50. IsPrivate bool
  51. }
  52. type HeaderDecl struct {
  53. // Comment contains a comment for a specific function. Each string is a
  54. // paragraph. Some paragraph may contain \n runes to indicate that they
  55. // are preformatted.
  56. Comment []string
  57. // Name contains the name of the function, if it could be extracted.
  58. Name string
  59. // Decl contains the preformatted C declaration itself.
  60. Decl string
  61. // Num is an index for the declaration, but the value is unique for all
  62. // declarations in a HeaderFile. It's included in order to help
  63. // text/template generate anchors.
  64. Num int
  65. }
  66. const (
  67. cppGuard = "#if defined(__cplusplus)"
  68. commentStart = "/* "
  69. commentEnd = " */"
  70. )
  71. func extractComment(lines []string, lineNo int) (comment []string, rest []string, restLineNo int, err error) {
  72. if len(lines) == 0 {
  73. return nil, lines, lineNo, nil
  74. }
  75. restLineNo = lineNo
  76. rest = lines
  77. if !strings.HasPrefix(rest[0], commentStart) {
  78. panic("extractComment called on non-comment")
  79. }
  80. commentParagraph := rest[0][len(commentStart):]
  81. rest = rest[1:]
  82. restLineNo++
  83. for len(rest) > 0 {
  84. i := strings.Index(commentParagraph, commentEnd)
  85. if i >= 0 {
  86. if i != len(commentParagraph)-len(commentEnd) {
  87. err = fmt.Errorf("garbage after comment end on line %d", restLineNo)
  88. return
  89. }
  90. commentParagraph = commentParagraph[:i]
  91. if len(commentParagraph) > 0 {
  92. comment = append(comment, commentParagraph)
  93. }
  94. return
  95. }
  96. line := rest[0]
  97. if !strings.HasPrefix(line, " *") {
  98. err = fmt.Errorf("comment doesn't start with block prefix on line %d: %s", restLineNo, line)
  99. return
  100. }
  101. line = line[2:]
  102. if strings.HasPrefix(line, " ") {
  103. /* Identing the lines of a paragraph marks them as
  104. * preformatted. */
  105. if len(commentParagraph) > 0 {
  106. commentParagraph += "\n"
  107. }
  108. line = line[3:]
  109. }
  110. if len(line) > 0 {
  111. commentParagraph = commentParagraph + line
  112. if len(commentParagraph) > 0 && commentParagraph[0] == ' ' {
  113. commentParagraph = commentParagraph[1:]
  114. }
  115. } else {
  116. comment = append(comment, commentParagraph)
  117. commentParagraph = ""
  118. }
  119. rest = rest[1:]
  120. restLineNo++
  121. }
  122. err = errors.New("hit EOF in comment")
  123. return
  124. }
  125. func extractDecl(lines []string, lineNo int) (decl string, rest []string, restLineNo int, err error) {
  126. if len(lines) == 0 {
  127. return "", lines, lineNo, nil
  128. }
  129. rest = lines
  130. restLineNo = lineNo
  131. var stack []rune
  132. for len(rest) > 0 {
  133. line := rest[0]
  134. for _, c := range line {
  135. switch c {
  136. case '(', '{', '[':
  137. stack = append(stack, c)
  138. case ')', '}', ']':
  139. if len(stack) == 0 {
  140. err = fmt.Errorf("unexpected %c on line %d", c, restLineNo)
  141. return
  142. }
  143. var expected rune
  144. switch c {
  145. case ')':
  146. expected = '('
  147. case '}':
  148. expected = '{'
  149. case ']':
  150. expected = '['
  151. default:
  152. panic("internal error")
  153. }
  154. if last := stack[len(stack)-1]; last != expected {
  155. err = fmt.Errorf("found %c when expecting %c on line %d", c, last, restLineNo)
  156. return
  157. }
  158. stack = stack[:len(stack)-1]
  159. }
  160. }
  161. if len(decl) > 0 {
  162. decl += "\n"
  163. }
  164. decl += line
  165. rest = rest[1:]
  166. restLineNo++
  167. if len(stack) == 0 && (len(decl) == 0 || decl[len(decl)-1] != '\\') {
  168. break
  169. }
  170. }
  171. return
  172. }
  173. func skipPast(s, skip string) string {
  174. i := strings.Index(s, skip)
  175. if i > 0 {
  176. return s[len(skip):]
  177. }
  178. return s
  179. }
  180. func getNameFromDecl(decl string) (string, bool) {
  181. if strings.HasPrefix(decl, "struct ") {
  182. return "", false
  183. }
  184. decl = skipPast(decl, "STACK_OF(")
  185. decl = skipPast(decl, "LHASH_OF(")
  186. i := strings.Index(decl, "(")
  187. if i < 0 {
  188. return "", false
  189. }
  190. j := strings.LastIndex(decl[:i], " ")
  191. if j < 0 {
  192. return "", false
  193. }
  194. for j+1 < len(decl) && decl[j+1] == '*' {
  195. j++
  196. }
  197. return decl[j+1 : i], true
  198. }
  199. func (config *Config) parseHeader(path string) (*HeaderFile, error) {
  200. headerPath := filepath.Join(config.BaseDirectory, path)
  201. headerFile, err := os.Open(headerPath)
  202. if err != nil {
  203. return nil, err
  204. }
  205. defer headerFile.Close()
  206. scanner := bufio.NewScanner(headerFile)
  207. var lines, oldLines []string
  208. for scanner.Scan() {
  209. lines = append(lines, scanner.Text())
  210. }
  211. if err := scanner.Err(); err != nil {
  212. return nil, err
  213. }
  214. lineNo := 0
  215. found := false
  216. for i, line := range lines {
  217. lineNo++
  218. if line == cppGuard {
  219. lines = lines[i+1:]
  220. lineNo++
  221. found = true
  222. break
  223. }
  224. }
  225. if !found {
  226. return nil, errors.New("no C++ guard found")
  227. }
  228. if len(lines) == 0 || lines[0] != "extern \"C\" {" {
  229. return nil, errors.New("no extern \"C\" found after C++ guard")
  230. }
  231. lineNo += 2
  232. lines = lines[2:]
  233. header := &HeaderFile{
  234. Name: filepath.Base(path),
  235. }
  236. for i, line := range lines {
  237. lineNo++
  238. if len(line) > 0 {
  239. lines = lines[i:]
  240. break
  241. }
  242. }
  243. oldLines = lines
  244. if len(lines) > 0 && strings.HasPrefix(lines[0], commentStart) {
  245. comment, rest, restLineNo, err := extractComment(lines, lineNo)
  246. if err != nil {
  247. return nil, err
  248. }
  249. if len(rest) > 0 && len(rest[0]) == 0 {
  250. if len(rest) < 2 || len(rest[1]) != 0 {
  251. return nil, errors.New("preamble comment should be followed by two blank lines")
  252. }
  253. header.Preamble = comment
  254. lineNo = restLineNo + 2
  255. lines = rest[2:]
  256. } else {
  257. lines = oldLines
  258. }
  259. }
  260. var sectionNumber, declNumber int
  261. for {
  262. // Start of a section.
  263. if len(lines) == 0 {
  264. return nil, errors.New("unexpected end of file")
  265. }
  266. line := lines[0]
  267. if line == cppGuard {
  268. break
  269. }
  270. if len(line) == 0 {
  271. return nil, fmt.Errorf("blank line at start of section on line %d", lineNo)
  272. }
  273. section := HeaderSection{
  274. Num: sectionNumber,
  275. }
  276. sectionNumber++
  277. if strings.HasPrefix(line, commentStart) {
  278. comment, rest, restLineNo, err := extractComment(lines, lineNo)
  279. if err != nil {
  280. return nil, err
  281. }
  282. if len(rest) > 0 && len(rest[0]) == 0 {
  283. section.Preamble = comment
  284. section.IsPrivate = len(comment) > 0 && strings.HasPrefix(comment[0], "Private functions")
  285. lines = rest[1:]
  286. lineNo = restLineNo + 1
  287. }
  288. }
  289. for len(lines) > 0 {
  290. line := lines[0]
  291. if len(line) == 0 {
  292. lines = lines[1:]
  293. lineNo++
  294. break
  295. }
  296. if line == cppGuard {
  297. return nil, errors.New("hit ending C++ guard while in section")
  298. }
  299. var comment []string
  300. var decl string
  301. if strings.HasPrefix(line, commentStart) {
  302. comment, lines, lineNo, err = extractComment(lines, lineNo)
  303. if err != nil {
  304. return nil, err
  305. }
  306. }
  307. if len(lines) == 0 {
  308. return nil, errors.New("expected decl at EOF")
  309. }
  310. decl, lines, lineNo, err = extractDecl(lines, lineNo)
  311. if err != nil {
  312. return nil, err
  313. }
  314. name, ok := getNameFromDecl(decl)
  315. if !ok {
  316. name = ""
  317. }
  318. if last := len(section.Decls) - 1; len(name) == 0 && len(comment) == 0 && last >= 0 {
  319. section.Decls[last].Decl += "\n" + decl
  320. } else {
  321. section.Decls = append(section.Decls, HeaderDecl{
  322. Comment: comment,
  323. Name: name,
  324. Decl: decl,
  325. Num: declNumber,
  326. })
  327. declNumber++
  328. }
  329. if len(lines) > 0 && len(lines[0]) == 0 {
  330. lines = lines[1:]
  331. lineNo++
  332. }
  333. }
  334. header.Sections = append(header.Sections, section)
  335. }
  336. return header, nil
  337. }
  338. func firstSentence(paragraphs []string) string {
  339. if len(paragraphs) == 0 {
  340. return ""
  341. }
  342. s := paragraphs[0]
  343. i := strings.Index(s, ". ")
  344. if i >= 0 {
  345. return s[:i]
  346. }
  347. if lastIndex := len(s) - 1; s[lastIndex] == '.' {
  348. return s[:lastIndex]
  349. }
  350. return s
  351. }
  352. func markupPipeWords(s string) template.HTML {
  353. ret := ""
  354. for {
  355. i := strings.Index(s, "|")
  356. if i == -1 {
  357. ret += s
  358. break
  359. }
  360. ret += s[:i]
  361. s = s[i+1:]
  362. i = strings.Index(s, "|")
  363. j := strings.Index(s, " ")
  364. if i > 0 && (j == -1 || j > i) {
  365. ret += "<tt>"
  366. ret += s[:i]
  367. ret += "</tt>"
  368. s = s[i+1:]
  369. } else {
  370. ret += "|"
  371. }
  372. }
  373. return template.HTML(ret)
  374. }
  375. func markupFirstWord(s template.HTML) template.HTML {
  376. i := strings.Index(string(s), " ")
  377. if i > 0 {
  378. return "<span class=\"first-word\">" + s[:i] + "</span>" + s[i:]
  379. }
  380. return s
  381. }
  382. func newlinesToBR(html template.HTML) template.HTML {
  383. s := string(html)
  384. if !strings.Contains(s, "\n") {
  385. return html
  386. }
  387. s = strings.Replace(s, "\n", "<br>", -1)
  388. s = strings.Replace(s, " ", "&nbsp;", -1)
  389. return template.HTML(s)
  390. }
  391. func generate(outPath string, config *Config) (map[string]string, error) {
  392. headerTmpl := template.New("headerTmpl")
  393. headerTmpl.Funcs(template.FuncMap{
  394. "firstSentence": firstSentence,
  395. "markupPipeWords": markupPipeWords,
  396. "markupFirstWord": markupFirstWord,
  397. "newlinesToBR": newlinesToBR,
  398. })
  399. headerTmpl, err := headerTmpl.Parse(`<!DOCTYPE html5>
  400. <html>
  401. <head>
  402. <title>BoringSSL - {{.Name}}</title>
  403. <meta charset="utf-8">
  404. <link rel="stylesheet" type="text/css" href="doc.css">
  405. </head>
  406. <body>
  407. <div id="main">
  408. <h2>{{.Name}}</h2>
  409. {{range .Preamble}}<p>{{. | html | markupPipeWords}}</p>{{end}}
  410. <ol>
  411. {{range .Sections}}
  412. {{if not .IsPrivate}}
  413. {{if .Preamble}}<li class="header"><a href="#section-{{.Num}}">{{.Preamble | firstSentence}}</a></li>{{end}}
  414. {{range .Decls}}
  415. {{if .Name}}<li><a href="#decl-{{.Num}}"><tt>{{.Name}}</tt></a></li>{{end}}
  416. {{end}}
  417. {{end}}
  418. {{end}}
  419. </ol>
  420. {{range .Sections}}
  421. {{if not .IsPrivate}}
  422. <div class="section">
  423. {{if .Preamble}}
  424. <div class="sectionpreamble">
  425. <a name="section-{{.Num}}">
  426. {{range .Preamble}}<p>{{. | html | markupPipeWords}}</p>{{end}}
  427. </a>
  428. </div>
  429. {{end}}
  430. {{range .Decls}}
  431. <div class="decl">
  432. <a name="decl-{{.Num}}">
  433. {{range .Comment}}
  434. <p>{{. | html | markupPipeWords | newlinesToBR | markupFirstWord}}</p>
  435. {{end}}
  436. <pre>{{.Decl}}</pre>
  437. </a>
  438. </div>
  439. {{end}}
  440. </div>
  441. {{end}}
  442. {{end}}
  443. </div>
  444. </body>
  445. </html>`)
  446. if err != nil {
  447. return nil, err
  448. }
  449. headerDescriptions := make(map[string]string)
  450. for _, section := range config.Sections {
  451. for _, headerPath := range section.Headers {
  452. header, err := config.parseHeader(headerPath)
  453. if err != nil {
  454. return nil, errors.New("while parsing " + headerPath + ": " + err.Error())
  455. }
  456. headerDescriptions[header.Name] = firstSentence(header.Preamble)
  457. filename := filepath.Join(outPath, header.Name+".html")
  458. file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
  459. if err != nil {
  460. panic(err)
  461. }
  462. defer file.Close()
  463. if err := headerTmpl.Execute(file, header); err != nil {
  464. return nil, err
  465. }
  466. }
  467. }
  468. return headerDescriptions, nil
  469. }
  470. func generateIndex(outPath string, config *Config, headerDescriptions map[string]string) error {
  471. indexTmpl := template.New("indexTmpl")
  472. indexTmpl.Funcs(template.FuncMap{
  473. "baseName": filepath.Base,
  474. "headerDescription": func(header string) string {
  475. return headerDescriptions[header]
  476. },
  477. })
  478. indexTmpl, err := indexTmpl.Parse(`<!DOCTYPE html5>
  479. <head>
  480. <title>BoringSSL - Headers</title>
  481. <meta charset="utf-8">
  482. <link rel="stylesheet" type="text/css" href="doc.css">
  483. </head>
  484. <body>
  485. <div id="main">
  486. <table>
  487. {{range .Sections}}
  488. <tr class="header"><td colspan="2">{{.Name}}</td></tr>
  489. {{range .Headers}}
  490. <tr><td><a href="{{. | baseName}}.html">{{. | baseName}}</a></td><td>{{. | baseName | headerDescription}}</td></tr>
  491. {{end}}
  492. {{end}}
  493. </table>
  494. </div>
  495. </body>
  496. </html>`)
  497. if err != nil {
  498. return err
  499. }
  500. file, err := os.OpenFile(filepath.Join(outPath, "headers.html"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
  501. if err != nil {
  502. panic(err)
  503. }
  504. defer file.Close()
  505. if err := indexTmpl.Execute(file, config); err != nil {
  506. return err
  507. }
  508. return nil
  509. }
  510. func main() {
  511. var (
  512. configFlag *string = flag.String("config", "", "Location of config file")
  513. outputDir *string = flag.String("out", "", "Path to the directory where the output will be written")
  514. config Config
  515. )
  516. flag.Parse()
  517. if len(*configFlag) == 0 {
  518. fmt.Printf("No config file given by --config\n")
  519. os.Exit(1)
  520. }
  521. if len(*outputDir) == 0 {
  522. fmt.Printf("No output directory given by --out\n")
  523. os.Exit(1)
  524. }
  525. configBytes, err := ioutil.ReadFile(*configFlag)
  526. if err != nil {
  527. fmt.Printf("Failed to open config file: %s\n", err)
  528. os.Exit(1)
  529. }
  530. if err := json.Unmarshal(configBytes, &config); err != nil {
  531. fmt.Printf("Failed to parse config file: %s\n", err)
  532. os.Exit(1)
  533. }
  534. headerDescriptions, err := generate(*outputDir, &config)
  535. if err != nil {
  536. fmt.Printf("Failed to generate output: %s\n", err)
  537. os.Exit(1)
  538. }
  539. if err := generateIndex(*outputDir, &config, headerDescriptions); err != nil {
  540. fmt.Printf("Failed to generate index: %s\n", err)
  541. os.Exit(1)
  542. }
  543. }