xref: /aosp_15_r20/external/boringssl/src/util/doc.go (revision 8fb009dc861624b67b6cdb62ea21f0f22d0c584b)
1//go:build ignore
2
3// doc generates HTML files from the comments in header files.
4//
5// doc expects to be given the path to a JSON file via the --config option.
6// From that JSON (which is defined by the Config struct) it reads a list of
7// header file locations and generates HTML files for each in the current
8// directory.
9package main
10
11import (
12	"bufio"
13	"encoding/json"
14	"errors"
15	"flag"
16	"fmt"
17	"html/template"
18	"os"
19	"path/filepath"
20	"regexp"
21	"strconv"
22	"strings"
23	"unicode"
24)
25
26// Config describes the structure of the config JSON file.
27type Config struct {
28	// BaseDirectory is a path to which other paths in the file are
29	// relative.
30	BaseDirectory string
31	Sections      []ConfigSection
32}
33
34type ConfigSection struct {
35	Name string
36	// Headers is a list of paths to header files.
37	Headers []string
38}
39
40// HeaderFile is the internal representation of a header file.
41type HeaderFile struct {
42	// Name is the basename of the header file (e.g. "ex_data.html").
43	Name string
44	// Preamble contains a comment for the file as a whole. Each string
45	// is a separate paragraph.
46	Preamble []CommentBlock
47	Sections []HeaderSection
48	// AllDecls maps all decls to their URL fragments.
49	AllDecls map[string]string
50}
51
52type HeaderSection struct {
53	// Preamble contains a comment for a group of functions.
54	Preamble []CommentBlock
55	Decls    []HeaderDecl
56	// Anchor, if non-empty, is the URL fragment to use in anchor tags.
57	Anchor string
58	// IsPrivate is true if the section contains private functions (as
59	// indicated by its name).
60	IsPrivate bool
61}
62
63type HeaderDecl struct {
64	// Comment contains a comment for a specific function. Each string is a
65	// paragraph. Some paragraph may contain \n runes to indicate that they
66	// are preformatted.
67	Comment []CommentBlock
68	// Name contains the name of the function, if it could be extracted.
69	Name string
70	// Decl contains the preformatted C declaration itself.
71	Decl string
72	// Anchor, if non-empty, is the URL fragment to use in anchor tags.
73	Anchor string
74}
75
76type CommentBlockType int
77
78const (
79	CommentParagraph CommentBlockType = iota
80	CommentOrderedListItem
81	CommentBulletListItem
82	CommentCode
83)
84
85type CommentBlock struct {
86	Type      CommentBlockType
87	Paragraph string
88}
89
90const (
91	cppGuard     = "#if defined(__cplusplus)"
92	commentStart = "/* "
93	commentEnd   = " */"
94	lineComment  = "// "
95)
96
97func isComment(line string) bool {
98	return strings.HasPrefix(line, commentStart) || strings.HasPrefix(line, lineComment)
99}
100
101func commentSubject(line string) string {
102	if strings.HasPrefix(line, "A ") {
103		line = line[len("A "):]
104	} else if strings.HasPrefix(line, "An ") {
105		line = line[len("An "):]
106	}
107	idx := strings.IndexAny(line, " ,")
108	if idx < 0 {
109		return line
110	}
111	return line[:idx]
112}
113
114func extractCommentLines(lines []string, lineNo int) (comment []string, rest []string, restLineNo int, err error) {
115	if len(lines) == 0 {
116		return nil, lines, lineNo, nil
117	}
118
119	restLineNo = lineNo
120	rest = lines
121
122	var isBlock bool
123	if strings.HasPrefix(rest[0], commentStart) {
124		isBlock = true
125	} else if !strings.HasPrefix(rest[0], lineComment) {
126		panic("extractComment called on non-comment")
127	}
128	comment = []string{rest[0][len(commentStart):]}
129	rest = rest[1:]
130	restLineNo++
131
132	for len(rest) > 0 {
133		if isBlock {
134			last := &comment[len(comment)-1]
135			if i := strings.Index(*last, commentEnd); i >= 0 {
136				if i != len(*last)-len(commentEnd) {
137					err = fmt.Errorf("garbage after comment end on line %d", restLineNo)
138					return
139				}
140				*last = (*last)[:i]
141				return
142			}
143		}
144
145		line := rest[0]
146		if isBlock {
147			if !strings.HasPrefix(line, " *") {
148				err = fmt.Errorf("comment doesn't start with block prefix on line %d: %s", restLineNo, line)
149				return
150			}
151		} else if !strings.HasPrefix(line, "//") {
152			return
153		}
154		comment = append(comment, line[2:])
155		rest = rest[1:]
156		restLineNo++
157	}
158
159	err = errors.New("hit EOF in comment")
160	return
161}
162
163func removeBulletListMarker(line string) (string, bool) {
164	orig := line
165	line = strings.TrimSpace(line)
166	if !strings.HasPrefix(line, "+ ") && !strings.HasPrefix(line, "- ") && !strings.HasPrefix(line, "* ") {
167		return orig, false
168	}
169	return line[2:], true
170}
171
172func removeOrderedListMarker(line string) (rest string, num int, ok bool) {
173	orig := line
174	line = strings.TrimSpace(line)
175	if len(line) == 0 || !unicode.IsDigit(rune(line[0])) {
176		return orig, -1, false
177	}
178
179	l := 0
180	for l < len(line) && unicode.IsDigit(rune(line[l])) {
181		l++
182	}
183	num, err := strconv.Atoi(line[:l])
184	if err != nil {
185		return orig, -1, false
186	}
187
188	line = line[l:]
189	if line, ok := strings.CutPrefix(line, ". "); ok {
190		return line, num, true
191	}
192	if line, ok := strings.CutPrefix(line, ") "); ok {
193		return line, num, true
194	}
195
196	return orig, -1, false
197}
198
199func removeCodeIndent(line string) (string, bool) {
200	return strings.CutPrefix(line, "   ")
201}
202
203func extractComment(lines []string, lineNo int) (comment []CommentBlock, rest []string, restLineNo int, err error) {
204	commentLines, rest, restLineNo, err := extractCommentLines(lines, lineNo)
205	if err != nil {
206		return
207	}
208
209	// This syntax and parsing algorithm is loosely inspired by CommonMark,
210	// but reduced to a small subset with no nesting. Blocks being open vs.
211	// closed can be tracked implicitly. We're also much slopplier about how
212	// indentation. Additionally, rather than grouping list items into
213	// lists, our parser just emits a list items, which are grouped later at
214	// rendering time.
215	//
216	// If we later need more features, such as nested lists, this can evolve
217	// into a more complex implementation.
218	var numBlankLines int
219	for _, line := range commentLines {
220		// Defer blank lines until we know the next element.
221		if len(strings.TrimSpace(line)) == 0 {
222			numBlankLines++
223			continue
224		}
225
226		blankLinesSkipped := numBlankLines
227		numBlankLines = 0
228
229		// Attempt to continue the previous block.
230		if len(comment) > 0 {
231			last := &comment[len(comment)-1]
232			if last.Type == CommentCode {
233				l, ok := removeCodeIndent(line)
234				if ok {
235					for i := 0; i < blankLinesSkipped; i++ {
236						last.Paragraph += "\n"
237					}
238					last.Paragraph += l + "\n"
239					continue
240				}
241			} else if blankLinesSkipped == 0 {
242				_, isBulletList := removeBulletListMarker(line)
243				_, num, isOrderedList := removeOrderedListMarker(line)
244				if isOrderedList && last.Type == CommentParagraph && num != 1 {
245					// A list item can only interrupt a paragraph if the number is one.
246					// See the discussion in https://spec.commonmark.org/0.30/#lists.
247					// This avoids wrapping like "(See RFC\n5280)" turning into a list.
248					isOrderedList = false
249				}
250				if !isBulletList && !isOrderedList {
251					// This is a continuation line of the previous paragraph.
252					last.Paragraph += " " + strings.TrimSpace(line)
253					continue
254				}
255			}
256		}
257
258		// Make a new block.
259		if line, ok := removeBulletListMarker(line); ok {
260			comment = append(comment, CommentBlock{
261				Type:      CommentBulletListItem,
262				Paragraph: strings.TrimSpace(line),
263			})
264		} else if line, _, ok := removeOrderedListMarker(line); ok {
265			comment = append(comment, CommentBlock{
266				Type:      CommentOrderedListItem,
267				Paragraph: strings.TrimSpace(line),
268			})
269		} else if line, ok := removeCodeIndent(line); ok {
270			comment = append(comment, CommentBlock{
271				Type:      CommentCode,
272				Paragraph: line + "\n",
273			})
274		} else {
275			comment = append(comment, CommentBlock{
276				Type:      CommentParagraph,
277				Paragraph: strings.TrimSpace(line),
278			})
279		}
280	}
281
282	return
283}
284
285func extractDecl(lines []string, lineNo int) (decl string, rest []string, restLineNo int, err error) {
286	if len(lines) == 0 || len(lines[0]) == 0 {
287		return "", lines, lineNo, nil
288	}
289
290	rest = lines
291	restLineNo = lineNo
292
293	var stack []rune
294	for len(rest) > 0 {
295		line := rest[0]
296		for _, c := range line {
297			switch c {
298			case '(', '{', '[':
299				stack = append(stack, c)
300			case ')', '}', ']':
301				if len(stack) == 0 {
302					err = fmt.Errorf("unexpected %c on line %d", c, restLineNo)
303					return
304				}
305				var expected rune
306				switch c {
307				case ')':
308					expected = '('
309				case '}':
310					expected = '{'
311				case ']':
312					expected = '['
313				default:
314					panic("internal error")
315				}
316				if last := stack[len(stack)-1]; last != expected {
317					err = fmt.Errorf("found %c when expecting %c on line %d", c, last, restLineNo)
318					return
319				}
320				stack = stack[:len(stack)-1]
321			}
322		}
323		if len(decl) > 0 {
324			decl += "\n"
325		}
326		decl += line
327		rest = rest[1:]
328		restLineNo++
329
330		if len(stack) == 0 && (len(decl) == 0 || decl[len(decl)-1] != '\\') {
331			break
332		}
333	}
334
335	return
336}
337
338func skipLine(s string) string {
339	i := strings.Index(s, "\n")
340	if i > 0 {
341		return s[i:]
342	}
343	return ""
344}
345
346var stackOfRegexp = regexp.MustCompile(`STACK_OF\(([^)]*)\)`)
347var lhashOfRegexp = regexp.MustCompile(`LHASH_OF\(([^)]*)\)`)
348
349func getNameFromDecl(decl string) (string, bool) {
350	for strings.HasPrefix(decl, "#if") || strings.HasPrefix(decl, "#elif") {
351		decl = skipLine(decl)
352	}
353
354	if strings.HasPrefix(decl, "typedef ") {
355		return "", false
356	}
357
358	for _, prefix := range []string{"struct ", "enum ", "#define "} {
359		if !strings.HasPrefix(decl, prefix) {
360			continue
361		}
362
363		decl = strings.TrimPrefix(decl, prefix)
364
365		for len(decl) > 0 && decl[0] == ' ' {
366			decl = decl[1:]
367		}
368
369		// struct and enum types can be the return type of a
370		// function.
371		if prefix[0] != '#' && strings.Index(decl, "{") == -1 {
372			break
373		}
374
375		i := strings.IndexAny(decl, "( ")
376		if i < 0 {
377			return "", false
378		}
379		return decl[:i], true
380	}
381	decl = strings.TrimPrefix(decl, "OPENSSL_EXPORT ")
382	decl = strings.TrimPrefix(decl, "const ")
383	decl = stackOfRegexp.ReplaceAllString(decl, "STACK_OF_$1")
384	decl = lhashOfRegexp.ReplaceAllString(decl, "LHASH_OF_$1")
385	i := strings.Index(decl, "(")
386	if i < 0 {
387		return "", false
388	}
389	j := strings.LastIndex(decl[:i], " ")
390	if j < 0 {
391		return "", false
392	}
393	for j+1 < len(decl) && decl[j+1] == '*' {
394		j++
395	}
396	return decl[j+1 : i], true
397}
398
399func sanitizeAnchor(name string) string {
400	return strings.Replace(name, " ", "-", -1)
401}
402
403func isPrivateSection(name string) bool {
404	return strings.HasPrefix(name, "Private functions") || strings.HasPrefix(name, "Private structures") || strings.Contains(name, "(hidden)")
405}
406
407func isCollectiveComment(line string) bool {
408	return strings.HasPrefix(line, "The ") || strings.HasPrefix(line, "These ")
409}
410
411func (config *Config) parseHeader(path string) (*HeaderFile, error) {
412	headerPath := filepath.Join(config.BaseDirectory, path)
413
414	headerFile, err := os.Open(headerPath)
415	if err != nil {
416		return nil, err
417	}
418	defer headerFile.Close()
419
420	scanner := bufio.NewScanner(headerFile)
421	var lines, oldLines []string
422	for scanner.Scan() {
423		lines = append(lines, scanner.Text())
424	}
425	if err := scanner.Err(); err != nil {
426		return nil, err
427	}
428
429	lineNo := 1
430	found := false
431	for i, line := range lines {
432		if line == cppGuard {
433			lines = lines[i+1:]
434			lineNo += i + 1
435			found = true
436			break
437		}
438	}
439
440	if !found {
441		return nil, errors.New("no C++ guard found")
442	}
443
444	if len(lines) == 0 || lines[0] != "extern \"C\" {" {
445		return nil, errors.New("no extern \"C\" found after C++ guard")
446	}
447	lineNo += 2
448	lines = lines[2:]
449
450	header := &HeaderFile{
451		Name:     filepath.Base(path),
452		AllDecls: make(map[string]string),
453	}
454
455	for i, line := range lines {
456		if len(line) > 0 {
457			lines = lines[i:]
458			lineNo += i
459			break
460		}
461	}
462
463	oldLines = lines
464	if len(lines) > 0 && isComment(lines[0]) {
465		comment, rest, restLineNo, err := extractComment(lines, lineNo)
466		if err != nil {
467			return nil, err
468		}
469
470		if len(rest) > 0 && len(rest[0]) == 0 {
471			if len(rest) < 2 || len(rest[1]) != 0 {
472				return nil, errors.New("preamble comment should be followed by two blank lines")
473			}
474			header.Preamble = comment
475			lineNo = restLineNo + 2
476			lines = rest[2:]
477		} else {
478			lines = oldLines
479		}
480	}
481
482	allAnchors := make(map[string]struct{})
483
484	for {
485		// Start of a section.
486		if len(lines) == 0 {
487			return nil, errors.New("unexpected end of file")
488		}
489		line := lines[0]
490		if line == cppGuard {
491			break
492		}
493
494		if len(line) == 0 {
495			return nil, fmt.Errorf("blank line at start of section on line %d", lineNo)
496		}
497
498		var section HeaderSection
499
500		if isComment(line) {
501			comment, rest, restLineNo, err := extractComment(lines, lineNo)
502			if err != nil {
503				return nil, err
504			}
505			if len(rest) > 0 && len(rest[0]) == 0 {
506				heading := firstSentence(comment)
507				anchor := sanitizeAnchor(heading)
508				if len(anchor) > 0 {
509					if _, ok := allAnchors[anchor]; ok {
510						return nil, fmt.Errorf("duplicate anchor: %s", anchor)
511					}
512					allAnchors[anchor] = struct{}{}
513				}
514
515				section.Preamble = comment
516				section.IsPrivate = isPrivateSection(heading)
517				section.Anchor = anchor
518				lines = rest[1:]
519				lineNo = restLineNo + 1
520			}
521		}
522
523		for len(lines) > 0 {
524			line := lines[0]
525			if len(line) == 0 {
526				lines = lines[1:]
527				lineNo++
528				break
529			}
530			if line == cppGuard {
531				return nil, fmt.Errorf("hit ending C++ guard while in section on line %d (possibly missing two empty lines ahead of guard?)", lineNo)
532			}
533
534			var comment []CommentBlock
535			var decl string
536			if isComment(line) {
537				comment, lines, lineNo, err = extractComment(lines, lineNo)
538				if err != nil {
539					return nil, err
540				}
541			}
542			if len(lines) == 0 {
543				return nil, fmt.Errorf("expected decl at EOF on line %d", lineNo)
544			}
545			declLineNo := lineNo
546			decl, lines, lineNo, err = extractDecl(lines, lineNo)
547			if err != nil {
548				return nil, err
549			}
550			name, ok := getNameFromDecl(decl)
551			if !ok {
552				name = ""
553			}
554			if last := len(section.Decls) - 1; len(name) == 0 && len(comment) == 0 && last >= 0 {
555				section.Decls[last].Decl += "\n" + decl
556			} else {
557				// As a matter of style, comments should start
558				// with the name of the thing that they are
559				// commenting on. We make an exception here for
560				// collective comments.
561				sentence := firstSentence(comment)
562				if len(comment) > 0 &&
563					len(name) > 0 &&
564					!isCollectiveComment(sentence) {
565					subject := commentSubject(sentence)
566					ok := subject == name
567					if l := len(subject); l > 0 && subject[l-1] == '*' {
568						// Groups of names, notably #defines, are often
569						// denoted with a wildcard.
570						ok = strings.HasPrefix(name, subject[:l-1])
571					}
572					if !ok {
573						return nil, fmt.Errorf("comment for %q doesn't seem to match line %s:%d\n", name, path, declLineNo)
574					}
575				}
576				anchor := sanitizeAnchor(name)
577				// TODO(davidben): Enforce uniqueness. This is
578				// skipped because #ifdefs currently result in
579				// duplicate table-of-contents entries.
580				allAnchors[anchor] = struct{}{}
581
582				header.AllDecls[name] = anchor
583
584				section.Decls = append(section.Decls, HeaderDecl{
585					Comment: comment,
586					Name:    name,
587					Decl:    decl,
588					Anchor:  anchor,
589				})
590			}
591
592			if len(lines) > 0 && len(lines[0]) == 0 {
593				lines = lines[1:]
594				lineNo++
595			}
596		}
597
598		header.Sections = append(header.Sections, section)
599	}
600
601	return header, nil
602}
603
604func firstSentence(comment []CommentBlock) string {
605	if len(comment) == 0 {
606		return ""
607	}
608	s := comment[0].Paragraph
609	i := strings.Index(s, ". ")
610	if i >= 0 {
611		return s[:i]
612	}
613	if lastIndex := len(s) - 1; s[lastIndex] == '.' {
614		return s[:lastIndex]
615	}
616	return s
617}
618
619func markupComment(allDecls map[string]string, comment []CommentBlock) template.HTML {
620	var b strings.Builder
621	lastType := CommentParagraph
622	closeList := func() {
623		if lastType == CommentOrderedListItem {
624			b.WriteString("</ol>")
625		} else if lastType == CommentBulletListItem {
626			b.WriteString("</ul>")
627		}
628	}
629
630	for _, block := range comment {
631		// Group consecutive list items of the same type into a list.
632		if block.Type != lastType {
633			closeList()
634			if block.Type == CommentOrderedListItem {
635				b.WriteString("<ol>")
636			} else if block.Type == CommentBulletListItem {
637				b.WriteString("<ul>")
638			}
639		}
640		lastType = block.Type
641
642		switch block.Type {
643		case CommentParagraph:
644			if strings.HasPrefix(block.Paragraph, "WARNING:") {
645				b.WriteString("<p class=\"warning\">")
646			} else {
647				b.WriteString("<p>")
648			}
649			b.WriteString(string(markupParagraph(allDecls, block.Paragraph)))
650			b.WriteString("</p>")
651		case CommentOrderedListItem, CommentBulletListItem:
652			b.WriteString("<li>")
653			b.WriteString(string(markupParagraph(allDecls, block.Paragraph)))
654			b.WriteString("</li>")
655		case CommentCode:
656			b.WriteString("<pre>")
657			b.WriteString(block.Paragraph)
658			b.WriteString("</pre>")
659		default:
660			panic(block.Type)
661		}
662	}
663
664	closeList()
665	return template.HTML(b.String())
666}
667
668func markupParagraph(allDecls map[string]string, s string) template.HTML {
669	// TODO(davidben): Ideally the inline transforms would be unified into
670	// one pass, so that the HTML output of one pass does not interfere with
671	// the next.
672	ret := markupPipeWords(allDecls, s, true /* linkDecls */)
673	ret = markupFirstWord(ret)
674	ret = markupRFC(ret)
675	return ret
676}
677
678// markupPipeWords converts |s| into an HTML string, safe to be included outside
679// a tag, while also marking up words surrounded by |.
680func markupPipeWords(allDecls map[string]string, s string, linkDecls bool) template.HTML {
681	// It is safe to look for '|' in the HTML-escaped version of |s|
682	// below. The escaped version cannot include '|' instead tags because
683	// there are no tags by construction.
684	s = template.HTMLEscapeString(s)
685	ret := ""
686
687	for {
688		i := strings.Index(s, "|")
689		if i == -1 {
690			ret += s
691			break
692		}
693		ret += s[:i]
694		s = s[i+1:]
695
696		i = strings.Index(s, "|")
697		j := strings.Index(s, " ")
698		if i > 0 && (j == -1 || j > i) {
699			ret += "<tt>"
700			anchor, isLink := allDecls[s[:i]]
701			if linkDecls && isLink {
702				ret += fmt.Sprintf("<a href=\"%s\">%s</a>", template.HTMLEscapeString(anchor), s[:i])
703			} else {
704				ret += s[:i]
705			}
706			ret += "</tt>"
707			s = s[i+1:]
708		} else {
709			ret += "|"
710		}
711	}
712
713	return template.HTML(ret)
714}
715
716func markupFirstWord(s template.HTML) template.HTML {
717	if isCollectiveComment(string(s)) {
718		return s
719	}
720	start := 0
721again:
722	end := strings.Index(string(s[start:]), " ")
723	if end > 0 {
724		end += start
725		w := strings.ToLower(string(s[start:end]))
726		// The first word was already marked up as an HTML tag. Don't
727		// mark it up further.
728		if strings.ContainsRune(w, '<') {
729			return s
730		}
731		if w == "a" || w == "an" {
732			start = end + 1
733			goto again
734		}
735		return s[:start] + "<span class=\"first-word\">" + s[start:end] + "</span>" + s[end:]
736	}
737	return s
738}
739
740var rfcRegexp = regexp.MustCompile("RFC ([0-9]+)")
741
742func markupRFC(html template.HTML) template.HTML {
743	s := string(html)
744	matches := rfcRegexp.FindAllStringSubmatchIndex(s, -1)
745	if len(matches) == 0 {
746		return html
747	}
748
749	var b strings.Builder
750	var idx int
751	for _, match := range matches {
752		start, end := match[0], match[1]
753		number := s[match[2]:match[3]]
754		b.WriteString(s[idx:start])
755		fmt.Fprintf(&b, "<a href=\"https://www.rfc-editor.org/rfc/rfc%s.html\">%s</a>", number, s[start:end])
756		idx = end
757	}
758	b.WriteString(s[idx:])
759	return template.HTML(b.String())
760}
761
762func generate(outPath string, config *Config) (map[string]string, error) {
763	allDecls := make(map[string]string)
764
765	headerTmpl := template.New("headerTmpl")
766	headerTmpl.Funcs(template.FuncMap{
767		"firstSentence":         firstSentence,
768		"markupPipeWordsNoLink": func(s string) template.HTML { return markupPipeWords(allDecls, s, false /* linkDecls */) },
769		"markupComment":         func(c []CommentBlock) template.HTML { return markupComment(allDecls, c) },
770	})
771	headerTmpl, err := headerTmpl.Parse(`<!DOCTYPE html>
772<html>
773  <head>
774    <title>BoringSSL - {{.Name}}</title>
775    <meta charset="utf-8">
776    <link rel="stylesheet" type="text/css" href="doc.css">
777  </head>
778
779  <body>
780    <div id="main">
781    <div class="title">
782      <h2>{{.Name}}</h2>
783      <a href="headers.html">All headers</a>
784    </div>
785
786    {{if .Preamble}}<div class="comment">{{.Preamble | markupComment}}</div>{{end}}
787
788    <ol class="toc">
789      {{range .Sections}}
790        {{if not .IsPrivate}}
791          {{if .Anchor}}<li class="header"><a href="#{{.Anchor}}">{{.Preamble | firstSentence | markupPipeWordsNoLink}}</a></li>{{end}}
792          {{range .Decls}}
793            {{if .Anchor}}<li><a href="#{{.Anchor}}"><tt>{{.Name}}</tt></a></li>{{end}}
794          {{end}}
795        {{end}}
796      {{end}}
797    </ol>
798
799    {{range .Sections}}
800      {{if not .IsPrivate}}
801        <div class="section" {{if .Anchor}}id="{{.Anchor}}"{{end}}>
802        {{if .Preamble}}<div class="sectionpreamble comment">{{.Preamble | markupComment}}</div>{{end}}
803
804        {{range .Decls}}
805          <div class="decl" {{if .Anchor}}id="{{.Anchor}}"{{end}}>
806            {{if .Comment}}<div class="comment">{{.Comment | markupComment}}</div>{{end}}
807            {{if .Decl}}<pre class="code">{{.Decl}}</pre>{{end}}
808          </div>
809        {{end}}
810        </div>
811      {{end}}
812    {{end}}
813    </div>
814  </body>
815</html>`)
816	if err != nil {
817		return nil, err
818	}
819
820	headerDescriptions := make(map[string]string)
821	var headers []*HeaderFile
822
823	for _, section := range config.Sections {
824		for _, headerPath := range section.Headers {
825			header, err := config.parseHeader(headerPath)
826			if err != nil {
827				return nil, errors.New("while parsing " + headerPath + ": " + err.Error())
828			}
829			headerDescriptions[header.Name] = firstSentence(header.Preamble)
830			headers = append(headers, header)
831
832			for name, anchor := range header.AllDecls {
833				allDecls[name] = fmt.Sprintf("%s#%s", header.Name+".html", anchor)
834			}
835		}
836	}
837
838	for _, header := range headers {
839		filename := filepath.Join(outPath, header.Name+".html")
840		file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
841		if err != nil {
842			panic(err)
843		}
844		defer file.Close()
845		if err := headerTmpl.Execute(file, header); err != nil {
846			return nil, err
847		}
848	}
849
850	return headerDescriptions, nil
851}
852
853func generateIndex(outPath string, config *Config, headerDescriptions map[string]string) error {
854	indexTmpl := template.New("indexTmpl")
855	indexTmpl.Funcs(template.FuncMap{
856		"baseName": filepath.Base,
857		"headerDescription": func(header string) string {
858			return headerDescriptions[header]
859		},
860	})
861	indexTmpl, err := indexTmpl.Parse(`<!DOCTYPE html5>
862
863  <head>
864    <title>BoringSSL - Headers</title>
865    <meta charset="utf-8">
866    <link rel="stylesheet" type="text/css" href="doc.css">
867  </head>
868
869  <body>
870    <div id="main">
871      <div class="title">
872        <h2>BoringSSL Headers</h2>
873      </div>
874      <table>
875        {{range .Sections}}
876	  <tr class="header"><td colspan="2">{{.Name}}</td></tr>
877	  {{range .Headers}}
878	    <tr><td><a href="{{. | baseName}}.html">{{. | baseName}}</a></td><td>{{. | baseName | headerDescription}}</td></tr>
879	  {{end}}
880	{{end}}
881      </table>
882    </div>
883  </body>
884</html>`)
885
886	if err != nil {
887		return err
888	}
889
890	file, err := os.OpenFile(filepath.Join(outPath, "headers.html"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
891	if err != nil {
892		panic(err)
893	}
894	defer file.Close()
895
896	if err := indexTmpl.Execute(file, config); err != nil {
897		return err
898	}
899
900	return nil
901}
902
903func copyFile(outPath string, inFilePath string) error {
904	bytes, err := os.ReadFile(inFilePath)
905	if err != nil {
906		return err
907	}
908	return os.WriteFile(filepath.Join(outPath, filepath.Base(inFilePath)), bytes, 0666)
909}
910
911func main() {
912	var (
913		configFlag *string = flag.String("config", "doc.config", "Location of config file")
914		outputDir  *string = flag.String("out", ".", "Path to the directory where the output will be written")
915		config     Config
916	)
917
918	flag.Parse()
919
920	if len(*configFlag) == 0 {
921		fmt.Printf("No config file given by --config\n")
922		os.Exit(1)
923	}
924
925	if len(*outputDir) == 0 {
926		fmt.Printf("No output directory given by --out\n")
927		os.Exit(1)
928	}
929
930	configBytes, err := os.ReadFile(*configFlag)
931	if err != nil {
932		fmt.Printf("Failed to open config file: %s\n", err)
933		os.Exit(1)
934	}
935
936	if err := json.Unmarshal(configBytes, &config); err != nil {
937		fmt.Printf("Failed to parse config file: %s\n", err)
938		os.Exit(1)
939	}
940
941	headerDescriptions, err := generate(*outputDir, &config)
942	if err != nil {
943		fmt.Printf("Failed to generate output: %s\n", err)
944		os.Exit(1)
945	}
946
947	if err := generateIndex(*outputDir, &config, headerDescriptions); err != nil {
948		fmt.Printf("Failed to generate index: %s\n", err)
949		os.Exit(1)
950	}
951
952	if err := copyFile(*outputDir, "doc.css"); err != nil {
953		fmt.Printf("Failed to copy static file: %s\n", err)
954		os.Exit(1)
955	}
956}
957