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
8package main
9
10import (
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.
24type 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
31type 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.
38type 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
47type 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
59type 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
74const (
75	cppGuard     = "#if defined(__cplusplus)"
76	commentStart = "/* "
77	commentEnd   = " */"
78)
79
80func 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		if len(line) == 2 || line[2] != '/' {
115			line = line[2:]
116		}
117		if strings.HasPrefix(line, "   ") {
118			/* Identing the lines of a paragraph marks them as
119			* preformatted. */
120			if len(commentParagraph) > 0 {
121				commentParagraph += "\n"
122			}
123			line = line[3:]
124		}
125		if len(line) > 0 {
126			commentParagraph = commentParagraph + line
127			if len(commentParagraph) > 0 && commentParagraph[0] == ' ' {
128				commentParagraph = commentParagraph[1:]
129			}
130		} else {
131			comment = append(comment, commentParagraph)
132			commentParagraph = ""
133		}
134		rest = rest[1:]
135		restLineNo++
136	}
137
138	err = errors.New("hit EOF in comment")
139	return
140}
141
142func extractDecl(lines []string, lineNo int) (decl string, rest []string, restLineNo int, err error) {
143	if len(lines) == 0 {
144		return "", lines, lineNo, nil
145	}
146
147	rest = lines
148	restLineNo = lineNo
149
150	var stack []rune
151	for len(rest) > 0 {
152		line := rest[0]
153		for _, c := range line {
154			switch c {
155			case '(', '{', '[':
156				stack = append(stack, c)
157			case ')', '}', ']':
158				if len(stack) == 0 {
159					err = fmt.Errorf("unexpected %c on line %d", c, restLineNo)
160					return
161				}
162				var expected rune
163				switch c {
164				case ')':
165					expected = '('
166				case '}':
167					expected = '{'
168				case ']':
169					expected = '['
170				default:
171					panic("internal error")
172				}
173				if last := stack[len(stack)-1]; last != expected {
174					err = fmt.Errorf("found %c when expecting %c on line %d", c, last, restLineNo)
175					return
176				}
177				stack = stack[:len(stack)-1]
178			}
179		}
180		if len(decl) > 0 {
181			decl += "\n"
182		}
183		decl += line
184		rest = rest[1:]
185		restLineNo++
186
187		if len(stack) == 0 && (len(decl) == 0 || decl[len(decl)-1] != '\\') {
188			break
189		}
190	}
191
192	return
193}
194
195func skipPast(s, skip string) string {
196	i := strings.Index(s, skip)
197	if i > 0 {
198		return s[i:]
199	}
200	return s
201}
202
203func skipLine(s string) string {
204	i := strings.Index(s, "\n")
205	if i > 0 {
206		return s[i:]
207	}
208	return ""
209}
210
211func getNameFromDecl(decl string) (string, bool) {
212	for strings.HasPrefix(decl, "#if") || strings.HasPrefix(decl, "#elif") {
213		decl = skipLine(decl)
214	}
215	if strings.HasPrefix(decl, "struct ") {
216		return "", false
217	}
218	if strings.HasPrefix(decl, "#define ") {
219		// This is a preprocessor #define. The name is the next symbol.
220		decl = strings.TrimPrefix(decl, "#define ")
221		for len(decl) > 0 && decl[0] == ' ' {
222			decl = decl[1:]
223		}
224		i := strings.IndexAny(decl, "( ")
225		if i < 0 {
226			return "", false
227		}
228		return decl[:i], true
229	}
230	decl = skipPast(decl, "STACK_OF(")
231	decl = skipPast(decl, "LHASH_OF(")
232	i := strings.Index(decl, "(")
233	if i < 0 {
234		return "", false
235	}
236	j := strings.LastIndex(decl[:i], " ")
237	if j < 0 {
238		return "", false
239	}
240	for j+1 < len(decl) && decl[j+1] == '*' {
241		j++
242	}
243	return decl[j+1 : i], true
244}
245
246func (config *Config) parseHeader(path string) (*HeaderFile, error) {
247	headerPath := filepath.Join(config.BaseDirectory, path)
248
249	headerFile, err := os.Open(headerPath)
250	if err != nil {
251		return nil, err
252	}
253	defer headerFile.Close()
254
255	scanner := bufio.NewScanner(headerFile)
256	var lines, oldLines []string
257	for scanner.Scan() {
258		lines = append(lines, scanner.Text())
259	}
260	if err := scanner.Err(); err != nil {
261		return nil, err
262	}
263
264	lineNo := 0
265	found := false
266	for i, line := range lines {
267		lineNo++
268		if line == cppGuard {
269			lines = lines[i+1:]
270			lineNo++
271			found = true
272			break
273		}
274	}
275
276	if !found {
277		return nil, errors.New("no C++ guard found")
278	}
279
280	if len(lines) == 0 || lines[0] != "extern \"C\" {" {
281		return nil, errors.New("no extern \"C\" found after C++ guard")
282	}
283	lineNo += 2
284	lines = lines[2:]
285
286	header := &HeaderFile{
287		Name: filepath.Base(path),
288	}
289
290	for i, line := range lines {
291		lineNo++
292		if len(line) > 0 {
293			lines = lines[i:]
294			break
295		}
296	}
297
298	oldLines = lines
299	if len(lines) > 0 && strings.HasPrefix(lines[0], commentStart) {
300		comment, rest, restLineNo, err := extractComment(lines, lineNo)
301		if err != nil {
302			return nil, err
303		}
304
305		if len(rest) > 0 && len(rest[0]) == 0 {
306			if len(rest) < 2 || len(rest[1]) != 0 {
307				return nil, errors.New("preamble comment should be followed by two blank lines")
308			}
309			header.Preamble = comment
310			lineNo = restLineNo + 2
311			lines = rest[2:]
312		} else {
313			lines = oldLines
314		}
315	}
316
317	var sectionNumber, declNumber int
318
319	for {
320		// Start of a section.
321		if len(lines) == 0 {
322			return nil, errors.New("unexpected end of file")
323		}
324		line := lines[0]
325		if line == cppGuard {
326			break
327		}
328
329		if len(line) == 0 {
330			return nil, fmt.Errorf("blank line at start of section on line %d", lineNo)
331		}
332
333		section := HeaderSection{
334			Num: sectionNumber,
335		}
336		sectionNumber++
337
338		if strings.HasPrefix(line, commentStart) {
339			comment, rest, restLineNo, err := extractComment(lines, lineNo)
340			if err != nil {
341				return nil, err
342			}
343			if len(rest) > 0 && len(rest[0]) == 0 {
344				section.Preamble = comment
345				section.IsPrivate = len(comment) > 0 && strings.HasPrefix(comment[0], "Private functions")
346				lines = rest[1:]
347				lineNo = restLineNo + 1
348			}
349		}
350
351		for len(lines) > 0 {
352			line := lines[0]
353			if len(line) == 0 {
354				lines = lines[1:]
355				lineNo++
356				break
357			}
358			if line == cppGuard {
359				return nil, errors.New("hit ending C++ guard while in section")
360			}
361
362			var comment []string
363			var decl string
364			if strings.HasPrefix(line, commentStart) {
365				comment, lines, lineNo, err = extractComment(lines, lineNo)
366				if err != nil {
367					return nil, err
368				}
369			}
370			if len(lines) == 0 {
371				return nil, errors.New("expected decl at EOF")
372			}
373			decl, lines, lineNo, err = extractDecl(lines, lineNo)
374			if err != nil {
375				return nil, err
376			}
377			name, ok := getNameFromDecl(decl)
378			if !ok {
379				name = ""
380			}
381			if last := len(section.Decls) - 1; len(name) == 0 && len(comment) == 0 && last >= 0 {
382				section.Decls[last].Decl += "\n" + decl
383			} else {
384				section.Decls = append(section.Decls, HeaderDecl{
385					Comment: comment,
386					Name:    name,
387					Decl:    decl,
388					Num:     declNumber,
389				})
390				declNumber++
391			}
392
393			if len(lines) > 0 && len(lines[0]) == 0 {
394				lines = lines[1:]
395				lineNo++
396			}
397		}
398
399		header.Sections = append(header.Sections, section)
400	}
401
402	return header, nil
403}
404
405func firstSentence(paragraphs []string) string {
406	if len(paragraphs) == 0 {
407		return ""
408	}
409	s := paragraphs[0]
410	i := strings.Index(s, ". ")
411	if i >= 0 {
412		return s[:i]
413	}
414	if lastIndex := len(s) - 1; s[lastIndex] == '.' {
415		return s[:lastIndex]
416	}
417	return s
418}
419
420func markupPipeWords(s string) template.HTML {
421	ret := ""
422
423	for {
424		i := strings.Index(s, "|")
425		if i == -1 {
426			ret += s
427			break
428		}
429		ret += s[:i]
430		s = s[i+1:]
431
432		i = strings.Index(s, "|")
433		j := strings.Index(s, " ")
434		if i > 0 && (j == -1 || j > i) {
435			ret += "<tt>"
436			ret += s[:i]
437			ret += "</tt>"
438			s = s[i+1:]
439		} else {
440			ret += "|"
441		}
442	}
443
444	return template.HTML(ret)
445}
446
447func markupFirstWord(s template.HTML) template.HTML {
448	start := 0
449again:
450	end := strings.Index(string(s[start:]), " ")
451	if end > 0 {
452		end += start
453		w := strings.ToLower(string(s[start:end]))
454		if w == "a" || w == "an" || w == "deprecated:" {
455			start = end + 1
456			goto again
457		}
458		return s[:start] + "<span class=\"first-word\">" + s[start:end] + "</span>" + s[end:]
459	}
460	return s
461}
462
463func newlinesToBR(html template.HTML) template.HTML {
464	s := string(html)
465	if !strings.Contains(s, "\n") {
466		return html
467	}
468	s = strings.Replace(s, "\n", "<br>", -1)
469	s = strings.Replace(s, " ", "&nbsp;", -1)
470	return template.HTML(s)
471}
472
473func generate(outPath string, config *Config) (map[string]string, error) {
474	headerTmpl := template.New("headerTmpl")
475	headerTmpl.Funcs(template.FuncMap{
476		"firstSentence":   firstSentence,
477		"markupPipeWords": markupPipeWords,
478		"markupFirstWord": markupFirstWord,
479		"newlinesToBR":    newlinesToBR,
480	})
481	headerTmpl, err := headerTmpl.Parse(`<!DOCTYPE html>
482<html>
483  <head>
484    <title>BoringSSL - {{.Name}}</title>
485    <meta charset="utf-8">
486    <link rel="stylesheet" type="text/css" href="doc.css">
487  </head>
488
489  <body>
490    <div id="main">
491    <h2>{{.Name}}</h2>
492
493    {{range .Preamble}}<p>{{. | html | markupPipeWords}}</p>{{end}}
494
495    <ol>
496      {{range .Sections}}
497        {{if not .IsPrivate}}
498          {{if .Preamble}}<li class="header"><a href="#section-{{.Num}}">{{.Preamble | firstSentence | html | markupPipeWords}}</a></li>{{end}}
499          {{range .Decls}}
500            {{if .Name}}<li><a href="#decl-{{.Num}}"><tt>{{.Name}}</tt></a></li>{{end}}
501          {{end}}
502        {{end}}
503      {{end}}
504    </ol>
505
506    {{range .Sections}}
507      {{if not .IsPrivate}}
508        <div class="section">
509        {{if .Preamble}}
510          <div class="sectionpreamble">
511          <a name="section-{{.Num}}">
512          {{range .Preamble}}<p>{{. | html | markupPipeWords}}</p>{{end}}
513          </a>
514          </div>
515        {{end}}
516
517        {{range .Decls}}
518          <div class="decl">
519          <a name="decl-{{.Num}}">
520          {{range .Comment}}
521            <p>{{. | html | markupPipeWords | newlinesToBR | markupFirstWord}}</p>
522          {{end}}
523          <pre>{{.Decl}}</pre>
524          </a>
525          </div>
526        {{end}}
527        </div>
528      {{end}}
529    {{end}}
530    </div>
531  </body>
532</html>`)
533	if err != nil {
534		return nil, err
535	}
536
537	headerDescriptions := make(map[string]string)
538
539	for _, section := range config.Sections {
540		for _, headerPath := range section.Headers {
541			header, err := config.parseHeader(headerPath)
542			if err != nil {
543				return nil, errors.New("while parsing " + headerPath + ": " + err.Error())
544			}
545			headerDescriptions[header.Name] = firstSentence(header.Preamble)
546			filename := filepath.Join(outPath, header.Name+".html")
547			file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
548			if err != nil {
549				panic(err)
550			}
551			defer file.Close()
552			if err := headerTmpl.Execute(file, header); err != nil {
553				return nil, err
554			}
555		}
556	}
557
558	return headerDescriptions, nil
559}
560
561func generateIndex(outPath string, config *Config, headerDescriptions map[string]string) error {
562	indexTmpl := template.New("indexTmpl")
563	indexTmpl.Funcs(template.FuncMap{
564		"baseName": filepath.Base,
565		"headerDescription": func(header string) string {
566			return headerDescriptions[header]
567		},
568	})
569	indexTmpl, err := indexTmpl.Parse(`<!DOCTYPE html5>
570
571  <head>
572    <title>BoringSSL - Headers</title>
573    <meta charset="utf-8">
574    <link rel="stylesheet" type="text/css" href="doc.css">
575  </head>
576
577  <body>
578    <div id="main">
579      <table>
580        {{range .Sections}}
581	  <tr class="header"><td colspan="2">{{.Name}}</td></tr>
582	  {{range .Headers}}
583	    <tr><td><a href="{{. | baseName}}.html">{{. | baseName}}</a></td><td>{{. | baseName | headerDescription}}</td></tr>
584	  {{end}}
585	{{end}}
586      </table>
587    </div>
588  </body>
589</html>`)
590
591	if err != nil {
592		return err
593	}
594
595	file, err := os.OpenFile(filepath.Join(outPath, "headers.html"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
596	if err != nil {
597		panic(err)
598	}
599	defer file.Close()
600
601	if err := indexTmpl.Execute(file, config); err != nil {
602		return err
603	}
604
605	return nil
606}
607
608func main() {
609	var (
610		configFlag *string = flag.String("config", "doc.config", "Location of config file")
611		outputDir  *string = flag.String("out", ".", "Path to the directory where the output will be written")
612		config     Config
613	)
614
615	flag.Parse()
616
617	if len(*configFlag) == 0 {
618		fmt.Printf("No config file given by --config\n")
619		os.Exit(1)
620	}
621
622	if len(*outputDir) == 0 {
623		fmt.Printf("No output directory given by --out\n")
624		os.Exit(1)
625	}
626
627	configBytes, err := ioutil.ReadFile(*configFlag)
628	if err != nil {
629		fmt.Printf("Failed to open config file: %s\n", err)
630		os.Exit(1)
631	}
632
633	if err := json.Unmarshal(configBytes, &config); err != nil {
634		fmt.Printf("Failed to parse config file: %s\n", err)
635		os.Exit(1)
636	}
637
638	headerDescriptions, err := generate(*outputDir, &config)
639	if err != nil {
640		fmt.Printf("Failed to generate output: %s\n", err)
641		os.Exit(1)
642	}
643
644	if err := generateIndex(*outputDir, &config, headerDescriptions); err != nil {
645		fmt.Printf("Failed to generate index: %s\n", err)
646		os.Exit(1)
647	}
648}
649