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