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 | or `.
680func markupPipeWords(allDecls map[string]string, s string, linkDecls bool) template.HTML {
681	// It is safe to look for '|' and '`' in the HTML-escaped version of |s|
682	// below. The escaped version cannot include '|' or '`' inside tags because
683	// there are no tags by construction.
684	s = template.HTMLEscapeString(s)
685	var ret strings.Builder
686
687	for {
688		i := strings.IndexAny(s, "|`")
689		if i == -1 {
690			ret.WriteString(s)
691			break
692		}
693		c := s[i]
694		ret.WriteString(s[:i])
695		s = s[i+1:]
696
697		i = strings.IndexByte(s, c)
698		j := strings.Index(s, " ")
699		if i > 0 && (j == -1 || j > i) {
700			ret.WriteString("<tt>")
701			anchor, isLink := allDecls[s[:i]]
702			if linkDecls && isLink {
703				fmt.Fprintf(&ret, "<a href=\"%s\">%s</a>", template.HTMLEscapeString(anchor), s[:i])
704			} else {
705				ret.WriteString(s[:i])
706			}
707			ret.WriteString("</tt>")
708			s = s[i+1:]
709		} else {
710			ret.WriteByte(c)
711		}
712	}
713
714	return template.HTML(ret.String())
715}
716
717func markupFirstWord(s template.HTML) template.HTML {
718	if isCollectiveComment(string(s)) {
719		return s
720	}
721	start := 0
722again:
723	end := strings.Index(string(s[start:]), " ")
724	if end > 0 {
725		end += start
726		w := strings.ToLower(string(s[start:end]))
727		// The first word was already marked up as an HTML tag. Don't
728		// mark it up further.
729		if strings.ContainsRune(w, '<') {
730			return s
731		}
732		if w == "a" || w == "an" {
733			start = end + 1
734			goto again
735		}
736		return s[:start] + "<span class=\"first-word\">" + s[start:end] + "</span>" + s[end:]
737	}
738	return s
739}
740
741var rfcRegexp = regexp.MustCompile("RFC ([0-9]+)")
742
743func markupRFC(html template.HTML) template.HTML {
744	s := string(html)
745	matches := rfcRegexp.FindAllStringSubmatchIndex(s, -1)
746	if len(matches) == 0 {
747		return html
748	}
749
750	var b strings.Builder
751	var idx int
752	for _, match := range matches {
753		start, end := match[0], match[1]
754		number := s[match[2]:match[3]]
755		b.WriteString(s[idx:start])
756		fmt.Fprintf(&b, "<a href=\"https://www.rfc-editor.org/rfc/rfc%s.html\">%s</a>", number, s[start:end])
757		idx = end
758	}
759	b.WriteString(s[idx:])
760	return template.HTML(b.String())
761}
762
763func generate(outPath string, config *Config) (map[string]string, error) {
764	allDecls := make(map[string]string)
765
766	headerTmpl := template.New("headerTmpl")
767	headerTmpl.Funcs(template.FuncMap{
768		"firstSentence":         firstSentence,
769		"markupPipeWordsNoLink": func(s string) template.HTML { return markupPipeWords(allDecls, s, false /* linkDecls */) },
770		"markupComment":         func(c []CommentBlock) template.HTML { return markupComment(allDecls, c) },
771	})
772	headerTmpl, err := headerTmpl.Parse(`<!DOCTYPE html>
773<html>
774  <head>
775    <title>BoringSSL - {{.Name}}</title>
776    <meta charset="utf-8">
777    <link rel="stylesheet" type="text/css" href="doc.css">
778  </head>
779
780  <body>
781    <div id="main">
782    <div class="title">
783      <h2>{{.Name}}</h2>
784      <a href="headers.html">All headers</a>
785    </div>
786
787    {{if .Preamble}}<div class="comment">{{.Preamble | markupComment}}</div>{{end}}
788
789    <ol class="toc">
790      {{range .Sections}}
791        {{if not .IsPrivate}}
792          {{if .Anchor}}<li class="header"><a href="#{{.Anchor}}">{{.Preamble | firstSentence | markupPipeWordsNoLink}}</a></li>{{end}}
793          {{range .Decls}}
794            {{if .Anchor}}<li><a href="#{{.Anchor}}"><tt>{{.Name}}</tt></a></li>{{end}}
795          {{end}}
796        {{end}}
797      {{end}}
798    </ol>
799
800    {{range .Sections}}
801      {{if not .IsPrivate}}
802        <div class="section" {{if .Anchor}}id="{{.Anchor}}"{{end}}>
803        {{if .Preamble}}<div class="sectionpreamble comment">{{.Preamble | markupComment}}</div>{{end}}
804
805        {{range .Decls}}
806          <div class="decl" {{if .Anchor}}id="{{.Anchor}}"{{end}}>
807            {{if .Comment}}<div class="comment">{{.Comment | markupComment}}</div>{{end}}
808            {{if .Decl}}<pre class="code">{{.Decl}}</pre>{{end}}
809          </div>
810        {{end}}
811        </div>
812      {{end}}
813    {{end}}
814    </div>
815  </body>
816</html>`)
817	if err != nil {
818		return nil, err
819	}
820
821	headerDescriptions := make(map[string]string)
822	var headers []*HeaderFile
823
824	for _, section := range config.Sections {
825		for _, headerPath := range section.Headers {
826			header, err := config.parseHeader(headerPath)
827			if err != nil {
828				return nil, errors.New("while parsing " + headerPath + ": " + err.Error())
829			}
830			headerDescriptions[header.Name] = firstSentence(header.Preamble)
831			headers = append(headers, header)
832
833			for name, anchor := range header.AllDecls {
834				allDecls[name] = fmt.Sprintf("%s#%s", header.Name+".html", anchor)
835			}
836		}
837	}
838
839	for _, header := range headers {
840		filename := filepath.Join(outPath, header.Name+".html")
841		file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
842		if err != nil {
843			panic(err)
844		}
845		defer file.Close()
846		if err := headerTmpl.Execute(file, header); err != nil {
847			return nil, err
848		}
849	}
850
851	return headerDescriptions, nil
852}
853
854func generateIndex(outPath string, config *Config, headerDescriptions map[string]string) error {
855	indexTmpl := template.New("indexTmpl")
856	indexTmpl.Funcs(template.FuncMap{
857		"baseName": filepath.Base,
858		"headerDescription": func(header string) string {
859			return headerDescriptions[header]
860		},
861	})
862	indexTmpl, err := indexTmpl.Parse(`<!DOCTYPE html5>
863
864  <head>
865    <title>BoringSSL - Headers</title>
866    <meta charset="utf-8">
867    <link rel="stylesheet" type="text/css" href="doc.css">
868  </head>
869
870  <body>
871    <div id="main">
872      <div class="title">
873        <h2>BoringSSL Headers</h2>
874      </div>
875      <table>
876        {{range .Sections}}
877	  <tr class="header"><td colspan="2">{{.Name}}</td></tr>
878	  {{range .Headers}}
879	    <tr><td><a href="{{. | baseName}}.html">{{. | baseName}}</a></td><td>{{. | baseName | headerDescription}}</td></tr>
880	  {{end}}
881	{{end}}
882      </table>
883    </div>
884  </body>
885</html>`)
886
887	if err != nil {
888		return err
889	}
890
891	file, err := os.OpenFile(filepath.Join(outPath, "headers.html"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
892	if err != nil {
893		panic(err)
894	}
895	defer file.Close()
896
897	if err := indexTmpl.Execute(file, config); err != nil {
898		return err
899	}
900
901	return nil
902}
903
904func copyFile(outPath string, inFilePath string) error {
905	bytes, err := os.ReadFile(inFilePath)
906	if err != nil {
907		return err
908	}
909	return os.WriteFile(filepath.Join(outPath, filepath.Base(inFilePath)), bytes, 0666)
910}
911
912func main() {
913	var (
914		configFlag *string = flag.String("config", "doc.config", "Location of config file")
915		outputDir  *string = flag.String("out", ".", "Path to the directory where the output will be written")
916		config     Config
917	)
918
919	flag.Parse()
920
921	if len(*configFlag) == 0 {
922		fmt.Printf("No config file given by --config\n")
923		os.Exit(1)
924	}
925
926	if len(*outputDir) == 0 {
927		fmt.Printf("No output directory given by --out\n")
928		os.Exit(1)
929	}
930
931	configBytes, err := os.ReadFile(*configFlag)
932	if err != nil {
933		fmt.Printf("Failed to open config file: %s\n", err)
934		os.Exit(1)
935	}
936
937	if err := json.Unmarshal(configBytes, &config); err != nil {
938		fmt.Printf("Failed to parse config file: %s\n", err)
939		os.Exit(1)
940	}
941
942	headerDescriptions, err := generate(*outputDir, &config)
943	if err != nil {
944		fmt.Printf("Failed to generate output: %s\n", err)
945		os.Exit(1)
946	}
947
948	if err := generateIndex(*outputDir, &config, headerDescriptions); err != nil {
949		fmt.Printf("Failed to generate index: %s\n", err)
950		os.Exit(1)
951	}
952
953	if err := copyFile(*outputDir, "doc.css"); err != nil {
954		fmt.Printf("Failed to copy static file: %s\n", err)
955		os.Exit(1)
956	}
957}
958