| //go:build ignore | |
| // doc generates HTML files from the comments in header files. | |
| // | |
| // doc expects to be given the path to a JSON file via the --config option. | |
| // From that JSON (which is defined by the Config struct) it reads a list of | |
| // header file locations and generates HTML files for each in the current | |
| // directory. | |
| package main | |
| import ( | |
| "bufio" | |
| "encoding/json" | |
| "errors" | |
| "flag" | |
| "fmt" | |
| "html/template" | |
| "os" | |
| "path/filepath" | |
| "regexp" | |
| "strconv" | |
| "strings" | |
| "unicode" | |
| ) | |
| // Config describes the structure of the config JSON file. | |
| type Config struct { | |
| // BaseDirectory is a path to which other paths in the file are | |
| // relative. | |
| BaseDirectory string | |
| Sections []ConfigSection | |
| } | |
| type ConfigSection struct { | |
| Name string | |
| // Headers is a list of paths to header files. | |
| Headers []string | |
| } | |
| // HeaderFile is the internal representation of a header file. | |
| type HeaderFile struct { | |
| // Name is the basename of the header file (e.g. "ex_data.html"). | |
| Name string | |
| // Preamble contains a comment for the file as a whole. Each string | |
| // is a separate paragraph. | |
| Preamble []CommentBlock | |
| Sections []HeaderSection | |
| // AllDecls maps all decls to their URL fragments. | |
| AllDecls map[string]string | |
| } | |
| type HeaderSection struct { | |
| // Preamble contains a comment for a group of functions. | |
| Preamble []CommentBlock | |
| Decls []HeaderDecl | |
| // Anchor, if non-empty, is the URL fragment to use in anchor tags. | |
| Anchor string | |
| // IsPrivate is true if the section contains private functions (as | |
| // indicated by its name). | |
| IsPrivate bool | |
| } | |
| type HeaderDecl struct { | |
| // Comment contains a comment for a specific function. Each string is a | |
| // paragraph. Some paragraph may contain \n runes to indicate that they | |
| // are preformatted. | |
| Comment []CommentBlock | |
| // Name contains the name of the function, if it could be extracted. | |
| Name string | |
| // Decl contains the preformatted C declaration itself. | |
| Decl string | |
| // Anchor, if non-empty, is the URL fragment to use in anchor tags. | |
| Anchor string | |
| } | |
| type CommentBlockType int | |
| const ( | |
| CommentParagraph CommentBlockType = iota | |
| CommentOrderedListItem | |
| CommentBulletListItem | |
| CommentCode | |
| ) | |
| type CommentBlock struct { | |
| Type CommentBlockType | |
| Paragraph string | |
| } | |
| const ( | |
| cppGuard = "#if defined(__cplusplus)" | |
| commentStart = "/* " | |
| commentEnd = " */" | |
| lineComment = "// " | |
| ) | |
| func isComment(line string) bool { | |
| return strings.HasPrefix(line, commentStart) || strings.HasPrefix(line, lineComment) | |
| } | |
| func commentSubject(line string) string { | |
| if strings.HasPrefix(line, "A ") { | |
| line = line[len("A "):] | |
| } else if strings.HasPrefix(line, "An ") { | |
| line = line[len("An "):] | |
| } | |
| idx := strings.IndexAny(line, " ,") | |
| if idx < 0 { | |
| return line | |
| } | |
| return line[:idx] | |
| } | |
| func extractCommentLines(lines []string, lineNo int) (comment []string, rest []string, restLineNo int, err error) { | |
| if len(lines) == 0 { | |
| return nil, lines, lineNo, nil | |
| } | |
| restLineNo = lineNo | |
| rest = lines | |
| var isBlock bool | |
| if strings.HasPrefix(rest[0], commentStart) { | |
| isBlock = true | |
| } else if !strings.HasPrefix(rest[0], lineComment) { | |
| panic("extractComment called on non-comment") | |
| } | |
| comment = []string{rest[0][len(commentStart):]} | |
| rest = rest[1:] | |
| restLineNo++ | |
| for len(rest) > 0 { | |
| if isBlock { | |
| last := &comment[len(comment)-1] | |
| if i := strings.Index(*last, commentEnd); i >= 0 { | |
| if i != len(*last)-len(commentEnd) { | |
| err = fmt.Errorf("garbage after comment end on line %d", restLineNo) | |
| return | |
| } | |
| *last = (*last)[:i] | |
| return | |
| } | |
| } | |
| line := rest[0] | |
| if isBlock { | |
| if !strings.HasPrefix(line, " *") { | |
| err = fmt.Errorf("comment doesn't start with block prefix on line %d: %s", restLineNo, line) | |
| return | |
| } | |
| } else if !strings.HasPrefix(line, "//") { | |
| return | |
| } | |
| comment = append(comment, line[2:]) | |
| rest = rest[1:] | |
| restLineNo++ | |
| } | |
| err = fmt.Errorf("hit EOF in comment on line %d", restLineNo) | |
| return | |
| } | |
| func removeBulletListMarker(line string) (string, bool) { | |
| orig := line | |
| line = strings.TrimSpace(line) | |
| if !strings.HasPrefix(line, "+ ") && !strings.HasPrefix(line, "- ") && !strings.HasPrefix(line, "* ") { | |
| return orig, false | |
| } | |
| return line[2:], true | |
| } | |
| func removeOrderedListMarker(line string) (rest string, num int, ok bool) { | |
| orig := line | |
| line = strings.TrimSpace(line) | |
| if len(line) == 0 || !unicode.IsDigit(rune(line[0])) { | |
| return orig, -1, false | |
| } | |
| l := 0 | |
| for l < len(line) && unicode.IsDigit(rune(line[l])) { | |
| l++ | |
| } | |
| num, err := strconv.Atoi(line[:l]) | |
| if err != nil { | |
| return orig, -1, false | |
| } | |
| line = line[l:] | |
| if line, ok := strings.CutPrefix(line, ". "); ok { | |
| return line, num, true | |
| } | |
| if line, ok := strings.CutPrefix(line, ") "); ok { | |
| return line, num, true | |
| } | |
| return orig, -1, false | |
| } | |
| func removeCodeIndent(line string) (string, bool) { | |
| return strings.CutPrefix(line, " ") | |
| } | |
| func extractComment(lines []string, lineNo int) (comment []CommentBlock, rest []string, restLineNo int, err error) { | |
| commentLines, rest, restLineNo, err := extractCommentLines(lines, lineNo) | |
| if err != nil { | |
| return | |
| } | |
| // This syntax and parsing algorithm is loosely inspired by CommonMark, | |
| // but reduced to a small subset with no nesting. Blocks being open vs. | |
| // closed can be tracked implicitly. We're also much slopplier about how | |
| // indentation. Additionally, rather than grouping list items into | |
| // lists, our parser just emits a list items, which are grouped later at | |
| // rendering time. | |
| // | |
| // If we later need more features, such as nested lists, this can evolve | |
| // into a more complex implementation. | |
| var numBlankLines int | |
| for _, line := range commentLines { | |
| // Defer blank lines until we know the next element. | |
| if len(strings.TrimSpace(line)) == 0 { | |
| numBlankLines++ | |
| continue | |
| } | |
| blankLinesSkipped := numBlankLines | |
| numBlankLines = 0 | |
| // Attempt to continue the previous block. | |
| if len(comment) > 0 { | |
| last := &comment[len(comment)-1] | |
| if last.Type == CommentCode { | |
| l, ok := removeCodeIndent(line) | |
| if ok { | |
| for i := 0; i < blankLinesSkipped; i++ { | |
| last.Paragraph += "\n" | |
| } | |
| last.Paragraph += l + "\n" | |
| continue | |
| } | |
| } else if blankLinesSkipped == 0 { | |
| _, isBulletList := removeBulletListMarker(line) | |
| _, num, isOrderedList := removeOrderedListMarker(line) | |
| if isOrderedList && last.Type == CommentParagraph && num != 1 { | |
| // A list item can only interrupt a paragraph if the number is one. | |
| // See the discussion in https://spec.commonmark.org/0.30/#lists. | |
| // This avoids wrapping like "(See RFC\n5280)" turning into a list. | |
| isOrderedList = false | |
| } | |
| if !isBulletList && !isOrderedList { | |
| // This is a continuation line of the previous paragraph. | |
| last.Paragraph += " " + strings.TrimSpace(line) | |
| continue | |
| } | |
| } | |
| } | |
| // Make a new block. | |
| if line, ok := removeBulletListMarker(line); ok { | |
| comment = append(comment, CommentBlock{ | |
| Type: CommentBulletListItem, | |
| Paragraph: strings.TrimSpace(line), | |
| }) | |
| } else if line, _, ok := removeOrderedListMarker(line); ok { | |
| comment = append(comment, CommentBlock{ | |
| Type: CommentOrderedListItem, | |
| Paragraph: strings.TrimSpace(line), | |
| }) | |
| } else if line, ok := removeCodeIndent(line); ok { | |
| comment = append(comment, CommentBlock{ | |
| Type: CommentCode, | |
| Paragraph: line + "\n", | |
| }) | |
| } else { | |
| comment = append(comment, CommentBlock{ | |
| Type: CommentParagraph, | |
| Paragraph: strings.TrimSpace(line), | |
| }) | |
| } | |
| } | |
| return | |
| } | |
| func extractDecl(lines []string, lineNo int) (decl string, rest []string, restLineNo int, err error) { | |
| if len(lines) == 0 || len(lines[0]) == 0 { | |
| return "", lines, lineNo, nil | |
| } | |
| rest = lines | |
| restLineNo = lineNo | |
| var stack []rune | |
| for len(rest) > 0 { | |
| line := rest[0] | |
| for _, c := range line { | |
| switch c { | |
| case '(', '{', '[': | |
| stack = append(stack, c) | |
| case ')', '}', ']': | |
| if len(stack) == 0 { | |
| err = fmt.Errorf("unexpected %c on line %d", c, restLineNo) | |
| return | |
| } | |
| var expected rune | |
| switch c { | |
| case ')': | |
| expected = '(' | |
| case '}': | |
| expected = '{' | |
| case ']': | |
| expected = '[' | |
| default: | |
| panic("internal error") | |
| } | |
| if last := stack[len(stack)-1]; last != expected { | |
| err = fmt.Errorf("found %c when expecting %c on line %d", c, last, restLineNo) | |
| return | |
| } | |
| stack = stack[:len(stack)-1] | |
| } | |
| } | |
| if len(decl) > 0 { | |
| decl += "\n" | |
| } | |
| decl += line | |
| rest = rest[1:] | |
| restLineNo++ | |
| if len(stack) == 0 && (len(decl) == 0 || decl[len(decl)-1] != '\\') { | |
| break | |
| } | |
| } | |
| return | |
| } | |
| func skipLine(s string) string { | |
| i := strings.Index(s, "\n") | |
| if i > 0 { | |
| return s[i:] | |
| } | |
| return "" | |
| } | |
| var stackOfRegexp = regexp.MustCompile(`STACK_OF\(([^)]*)\)`) | |
| var lhashOfRegexp = regexp.MustCompile(`LHASH_OF\(([^)]*)\)`) | |
| func getNameFromDecl(decl string) (string, bool) { | |
| for strings.HasPrefix(decl, "#if") || strings.HasPrefix(decl, "#elif") { | |
| decl = skipLine(decl) | |
| } | |
| if strings.HasPrefix(decl, "typedef ") { | |
| return "", false | |
| } | |
| for _, prefix := range []string{"struct ", "enum ", "#define "} { | |
| if !strings.HasPrefix(decl, prefix) { | |
| continue | |
| } | |
| decl = strings.TrimPrefix(decl, prefix) | |
| for len(decl) > 0 && decl[0] == ' ' { | |
| decl = decl[1:] | |
| } | |
| // struct and enum types can be the return type of a | |
| // function. | |
| if prefix[0] != '#' && strings.Index(decl, "{") == -1 { | |
| break | |
| } | |
| i := strings.IndexAny(decl, "( ") | |
| if i < 0 { | |
| return "", false | |
| } | |
| return decl[:i], true | |
| } | |
| decl = strings.TrimPrefix(decl, "OPENSSL_EXPORT ") | |
| decl = strings.TrimPrefix(decl, "const ") | |
| decl = stackOfRegexp.ReplaceAllString(decl, "STACK_OF_$1") | |
| decl = lhashOfRegexp.ReplaceAllString(decl, "LHASH_OF_$1") | |
| i := strings.Index(decl, "(") | |
| if i < 0 { | |
| return "", false | |
| } | |
| j := strings.LastIndex(decl[:i], " ") | |
| if j < 0 { | |
| return "", false | |
| } | |
| for j+1 < len(decl) && decl[j+1] == '*' { | |
| j++ | |
| } | |
| return decl[j+1 : i], true | |
| } | |
| func sanitizeAnchor(name string) string { | |
| return strings.Replace(name, " ", "-", -1) | |
| } | |
| func isPrivateSection(name string) bool { | |
| return strings.HasPrefix(name, "Private functions") || strings.HasPrefix(name, "Private structures") || strings.Contains(name, "(hidden)") | |
| } | |
| func isCollectiveComment(line string) bool { | |
| return strings.HasPrefix(line, "The ") || strings.HasPrefix(line, "These ") | |
| } | |
| func (config *Config) parseHeader(path string) (*HeaderFile, error) { | |
| headerPath := filepath.Join(config.BaseDirectory, path) | |
| headerFile, err := os.Open(headerPath) | |
| if err != nil { | |
| return nil, err | |
| } | |
| defer headerFile.Close() | |
| scanner := bufio.NewScanner(headerFile) | |
| var lines, oldLines []string | |
| for scanner.Scan() { | |
| lines = append(lines, scanner.Text()) | |
| } | |
| if err := scanner.Err(); err != nil { | |
| return nil, err | |
| } | |
| lineNo := 1 | |
| found := false | |
| for i, line := range lines { | |
| if line == cppGuard { | |
| lines = lines[i+1:] | |
| lineNo += i + 1 | |
| found = true | |
| break | |
| } | |
| } | |
| if !found { | |
| return nil, errors.New("no C++ guard found") | |
| } | |
| if len(lines) == 0 || lines[0] != "extern \"C\" {" { | |
| return nil, fmt.Errorf("no extern \"C\" found after C++ guard on line %d", lineNo) | |
| } | |
| lineNo += 2 | |
| lines = lines[2:] | |
| header := &HeaderFile{ | |
| Name: filepath.Base(path), | |
| AllDecls: make(map[string]string), | |
| } | |
| for i, line := range lines { | |
| if len(line) > 0 { | |
| lines = lines[i:] | |
| lineNo += i | |
| break | |
| } | |
| } | |
| oldLines = lines | |
| if len(lines) > 0 && isComment(lines[0]) { | |
| comment, rest, restLineNo, err := extractComment(lines, lineNo) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if len(rest) > 0 && len(rest[0]) == 0 { | |
| if len(rest) < 2 || len(rest[1]) != 0 { | |
| return nil, fmt.Errorf("preamble comment should be followed by two blank lines on line %d", restLineNo) | |
| } | |
| header.Preamble = comment | |
| lineNo = restLineNo + 2 | |
| lines = rest[2:] | |
| } else { | |
| lines = oldLines | |
| } | |
| } | |
| allAnchors := make(map[string]struct{}) | |
| for { | |
| // Start of a section. | |
| if len(lines) == 0 { | |
| return nil, fmt.Errorf("unexpected end of file on line %d", lineNo) | |
| } | |
| line := lines[0] | |
| if line == cppGuard { | |
| break | |
| } | |
| if len(line) == 0 { | |
| return nil, fmt.Errorf("blank line at start of section on line %d", lineNo) | |
| } | |
| var section HeaderSection | |
| if isComment(line) { | |
| comment, rest, restLineNo, err := extractComment(lines, lineNo) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if len(rest) > 0 && len(rest[0]) == 0 { | |
| heading := firstSentence(comment) | |
| anchor := sanitizeAnchor(heading) | |
| if len(anchor) > 0 { | |
| if _, ok := allAnchors[anchor]; ok { | |
| return nil, fmt.Errorf("duplicate anchor: %s", anchor) | |
| } | |
| allAnchors[anchor] = struct{}{} | |
| } | |
| section.Preamble = comment | |
| section.IsPrivate = isPrivateSection(heading) | |
| section.Anchor = anchor | |
| lines = rest[1:] | |
| lineNo = restLineNo + 1 | |
| } | |
| } | |
| for len(lines) > 0 { | |
| line := lines[0] | |
| if len(line) == 0 { | |
| lines = lines[1:] | |
| lineNo++ | |
| break | |
| } | |
| if line == cppGuard { | |
| return nil, fmt.Errorf("hit ending C++ guard while in section on line %d (possibly missing two empty lines ahead of guard?)", lineNo) | |
| } | |
| var comment []CommentBlock | |
| var decl string | |
| if isComment(line) { | |
| comment, lines, lineNo, err = extractComment(lines, lineNo) | |
| if err != nil { | |
| return nil, err | |
| } | |
| } | |
| if len(lines) == 0 { | |
| return nil, fmt.Errorf("expected decl at EOF on line %d", lineNo) | |
| } | |
| declLineNo := lineNo | |
| decl, lines, lineNo, err = extractDecl(lines, lineNo) | |
| if err != nil { | |
| return nil, err | |
| } | |
| name, ok := getNameFromDecl(decl) | |
| if !ok { | |
| name = "" | |
| } | |
| if last := len(section.Decls) - 1; len(name) == 0 && len(comment) == 0 && last >= 0 { | |
| section.Decls[last].Decl += "\n" + decl | |
| } else { | |
| // As a matter of style, comments should start | |
| // with the name of the thing that they are | |
| // commenting on. We make an exception here for | |
| // collective comments. | |
| sentence := firstSentence(comment) | |
| if len(comment) > 0 && | |
| len(name) > 0 && | |
| !isCollectiveComment(sentence) { | |
| subject := commentSubject(sentence) | |
| ok := subject == name | |
| if l := len(subject); l > 0 && subject[l-1] == '*' { | |
| // Groups of names, notably #defines, are often | |
| // denoted with a wildcard. | |
| ok = strings.HasPrefix(name, subject[:l-1]) | |
| } | |
| if !ok { | |
| return nil, fmt.Errorf("comment for %q doesn't seem to match line %s:%d\n", name, path, declLineNo) | |
| } | |
| } | |
| anchor := sanitizeAnchor(name) | |
| // TODO(davidben): Enforce uniqueness. This is | |
| // skipped because #ifdefs currently result in | |
| // duplicate table-of-contents entries. | |
| allAnchors[anchor] = struct{}{} | |
| header.AllDecls[name] = anchor | |
| section.Decls = append(section.Decls, HeaderDecl{ | |
| Comment: comment, | |
| Name: name, | |
| Decl: decl, | |
| Anchor: anchor, | |
| }) | |
| } | |
| if len(lines) > 0 && len(lines[0]) == 0 { | |
| lines = lines[1:] | |
| lineNo++ | |
| } | |
| } | |
| header.Sections = append(header.Sections, section) | |
| } | |
| return header, nil | |
| } | |
| func firstSentence(comment []CommentBlock) string { | |
| if len(comment) == 0 { | |
| return "" | |
| } | |
| s := comment[0].Paragraph | |
| i := strings.Index(s, ". ") | |
| if i >= 0 { | |
| return s[:i] | |
| } | |
| if lastIndex := len(s) - 1; s[lastIndex] == '.' { | |
| return s[:lastIndex] | |
| } | |
| return s | |
| } | |
| func markupComment(allDecls map[string]string, comment []CommentBlock) template.HTML { | |
| var b strings.Builder | |
| lastType := CommentParagraph | |
| closeList := func() { | |
| if lastType == CommentOrderedListItem { | |
| b.WriteString("</ol>") | |
| } else if lastType == CommentBulletListItem { | |
| b.WriteString("</ul>") | |
| } | |
| } | |
| for _, block := range comment { | |
| // Group consecutive list items of the same type into a list. | |
| if block.Type != lastType { | |
| closeList() | |
| if block.Type == CommentOrderedListItem { | |
| b.WriteString("<ol>") | |
| } else if block.Type == CommentBulletListItem { | |
| b.WriteString("<ul>") | |
| } | |
| } | |
| lastType = block.Type | |
| switch block.Type { | |
| case CommentParagraph: | |
| if strings.HasPrefix(block.Paragraph, "WARNING:") { | |
| b.WriteString("<p class=\"warning\">") | |
| } else { | |
| b.WriteString("<p>") | |
| } | |
| b.WriteString(string(markupParagraph(allDecls, block.Paragraph))) | |
| b.WriteString("</p>") | |
| case CommentOrderedListItem, CommentBulletListItem: | |
| b.WriteString("<li>") | |
| b.WriteString(string(markupParagraph(allDecls, block.Paragraph))) | |
| b.WriteString("</li>") | |
| case CommentCode: | |
| b.WriteString("<pre>") | |
| b.WriteString(block.Paragraph) | |
| b.WriteString("</pre>") | |
| default: | |
| panic(block.Type) | |
| } | |
| } | |
| closeList() | |
| return template.HTML(b.String()) | |
| } | |
| func markupParagraph(allDecls map[string]string, s string) template.HTML { | |
| // TODO(davidben): Ideally the inline transforms would be unified into | |
| // one pass, so that the HTML output of one pass does not interfere with | |
| // the next. | |
| ret := markupPipeWords(allDecls, s, true /* linkDecls */) | |
| ret = markupFirstWord(ret) | |
| ret = markupRFC(ret) | |
| return ret | |
| } | |
| // markupPipeWords converts |s| into an HTML string, safe to be included outside | |
| // a tag, while also marking up words surrounded by | or `. | |
| func markupPipeWords(allDecls map[string]string, s string, linkDecls bool) template.HTML { | |
| // It is safe to look for '|' and '`' in the HTML-escaped version of |s| | |
| // below. The escaped version cannot include '|' or '`' inside tags because | |
| // there are no tags by construction. | |
| s = template.HTMLEscapeString(s) | |
| var ret strings.Builder | |
| for { | |
| i := strings.IndexAny(s, "|`") | |
| if i == -1 { | |
| ret.WriteString(s) | |
| break | |
| } | |
| c := s[i] | |
| ret.WriteString(s[:i]) | |
| s = s[i+1:] | |
| i = strings.IndexByte(s, c) | |
| j := strings.Index(s, " ") | |
| if i > 0 && (j == -1 || j > i) { | |
| ret.WriteString("<tt>") | |
| anchor, isLink := allDecls[s[:i]] | |
| if linkDecls && isLink { | |
| fmt.Fprintf(&ret, "<a href=\"%s\">%s</a>", template.HTMLEscapeString(anchor), s[:i]) | |
| } else { | |
| ret.WriteString(s[:i]) | |
| } | |
| ret.WriteString("</tt>") | |
| s = s[i+1:] | |
| } else { | |
| ret.WriteByte(c) | |
| } | |
| } | |
| return template.HTML(ret.String()) | |
| } | |
| func markupFirstWord(s template.HTML) template.HTML { | |
| if isCollectiveComment(string(s)) { | |
| return s | |
| } | |
| start := 0 | |
| again: | |
| end := strings.Index(string(s[start:]), " ") | |
| if end > 0 { | |
| end += start | |
| w := strings.ToLower(string(s[start:end])) | |
| // The first word was already marked up as an HTML tag. Don't | |
| // mark it up further. | |
| if strings.ContainsRune(w, '<') { | |
| return s | |
| } | |
| if w == "a" || w == "an" { | |
| start = end + 1 | |
| goto again | |
| } | |
| return s[:start] + "<span class=\"first-word\">" + s[start:end] + "</span>" + s[end:] | |
| } | |
| return s | |
| } | |
| var rfcRegexp = regexp.MustCompile("RFC ([0-9]+)") | |
| func markupRFC(html template.HTML) template.HTML { | |
| s := string(html) | |
| matches := rfcRegexp.FindAllStringSubmatchIndex(s, -1) | |
| if len(matches) == 0 { | |
| return html | |
| } | |
| var b strings.Builder | |
| var idx int | |
| for _, match := range matches { | |
| start, end := match[0], match[1] | |
| number := s[match[2]:match[3]] | |
| b.WriteString(s[idx:start]) | |
| fmt.Fprintf(&b, "<a href=\"https://www.rfc-editor.org/rfc/rfc%s.html\">%s</a>", number, s[start:end]) | |
| idx = end | |
| } | |
| b.WriteString(s[idx:]) | |
| return template.HTML(b.String()) | |
| } | |
| func generate(outPath string, config *Config) (map[string]string, error) { | |
| allDecls := make(map[string]string) | |
| headerTmpl := template.New("headerTmpl") | |
| headerTmpl.Funcs(template.FuncMap{ | |
| "firstSentence": firstSentence, | |
| "markupPipeWordsNoLink": func(s string) template.HTML { return markupPipeWords(allDecls, s, false /* linkDecls */) }, | |
| "markupComment": func(c []CommentBlock) template.HTML { return markupComment(allDecls, c) }, | |
| }) | |
| headerTmpl, err := headerTmpl.Parse(`<!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>BoringSSL - {{.Name}}</title> | |
| <meta charset="utf-8"> | |
| <link rel="stylesheet" type="text/css" href="doc.css"> | |
| </head> | |
| <body> | |
| <div id="main"> | |
| <div class="title"> | |
| <h2>{{.Name}}</h2> | |
| <a href="headers.html">All headers</a> | |
| </div> | |
| {{if .Preamble}}<div class="comment">{{.Preamble | markupComment}}</div>{{end}} | |
| <ol class="toc"> | |
| {{range .Sections}} | |
| {{if not .IsPrivate}} | |
| {{if .Anchor}}<li class="header"><a href="#{{.Anchor}}">{{.Preamble | firstSentence | markupPipeWordsNoLink}}</a></li>{{end}} | |
| {{range .Decls}} | |
| {{if .Anchor}}<li><a href="#{{.Anchor}}"><tt>{{.Name}}</tt></a></li>{{end}} | |
| {{end}} | |
| {{end}} | |
| {{end}} | |
| </ol> | |
| {{range .Sections}} | |
| {{if not .IsPrivate}} | |
| <div class="section" {{if .Anchor}}id="{{.Anchor}}"{{end}}> | |
| {{if .Preamble}}<div class="sectionpreamble comment">{{.Preamble | markupComment}}</div>{{end}} | |
| {{range .Decls}} | |
| <div class="decl" {{if .Anchor}}id="{{.Anchor}}"{{end}}> | |
| {{if .Comment}}<div class="comment">{{.Comment | markupComment}}</div>{{end}} | |
| {{if .Decl}}<pre class="code">{{.Decl}}</pre>{{end}} | |
| </div> | |
| {{end}} | |
| </div> | |
| {{end}} | |
| {{end}} | |
| </div> | |
| </body> | |
| </html>`) | |
| if err != nil { | |
| return nil, err | |
| } | |
| headerDescriptions := make(map[string]string) | |
| var headers []*HeaderFile | |
| for _, section := range config.Sections { | |
| for _, headerPath := range section.Headers { | |
| header, err := config.parseHeader(headerPath) | |
| if err != nil { | |
| return nil, errors.New("while parsing " + headerPath + ": " + err.Error()) | |
| } | |
| headerDescriptions[header.Name] = firstSentence(header.Preamble) | |
| headers = append(headers, header) | |
| for name, anchor := range header.AllDecls { | |
| allDecls[name] = fmt.Sprintf("%s#%s", header.Name+".html", anchor) | |
| } | |
| } | |
| } | |
| for _, header := range headers { | |
| filename := filepath.Join(outPath, header.Name+".html") | |
| file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) | |
| if err != nil { | |
| panic(err) | |
| } | |
| defer file.Close() | |
| if err := headerTmpl.Execute(file, header); err != nil { | |
| return nil, err | |
| } | |
| } | |
| return headerDescriptions, nil | |
| } | |
| func generateIndex(outPath string, config *Config, headerDescriptions map[string]string) error { | |
| indexTmpl := template.New("indexTmpl") | |
| indexTmpl.Funcs(template.FuncMap{ | |
| "baseName": filepath.Base, | |
| "headerDescription": func(header string) string { | |
| return headerDescriptions[header] | |
| }, | |
| }) | |
| indexTmpl, err := indexTmpl.Parse(`<!DOCTYPE html5> | |
| <head> | |
| <title>BoringSSL - Headers</title> | |
| <meta charset="utf-8"> | |
| <link rel="stylesheet" type="text/css" href="doc.css"> | |
| </head> | |
| <body> | |
| <div id="main"> | |
| <div class="title"> | |
| <h2>BoringSSL Headers</h2> | |
| </div> | |
| <table> | |
| {{range .Sections}} | |
| <tr class="header"><td colspan="2">{{.Name}}</td></tr> | |
| {{range .Headers}} | |
| <tr><td><a href="{{. | baseName}}.html">{{. | baseName}}</a></td><td>{{. | baseName | headerDescription}}</td></tr> | |
| {{end}} | |
| {{end}} | |
| </table> | |
| </div> | |
| </body> | |
| </html>`) | |
| if err != nil { | |
| return err | |
| } | |
| file, err := os.OpenFile(filepath.Join(outPath, "headers.html"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) | |
| if err != nil { | |
| panic(err) | |
| } | |
| defer file.Close() | |
| if err := indexTmpl.Execute(file, config); err != nil { | |
| return err | |
| } | |
| return nil | |
| } | |
| func copyFile(outPath string, inFilePath string) error { | |
| bytes, err := os.ReadFile(inFilePath) | |
| if err != nil { | |
| return err | |
| } | |
| return os.WriteFile(filepath.Join(outPath, filepath.Base(inFilePath)), bytes, 0666) | |
| } | |
| func main() { | |
| var ( | |
| configFlag *string = flag.String("config", "doc.config", "Location of config file") | |
| outputDir *string = flag.String("out", ".", "Path to the directory where the output will be written") | |
| config Config | |
| ) | |
| flag.Parse() | |
| if len(*configFlag) == 0 { | |
| fmt.Printf("No config file given by --config\n") | |
| os.Exit(1) | |
| } | |
| if len(*outputDir) == 0 { | |
| fmt.Printf("No output directory given by --out\n") | |
| os.Exit(1) | |
| } | |
| configBytes, err := os.ReadFile(*configFlag) | |
| if err != nil { | |
| fmt.Printf("Failed to open config file: %s\n", err) | |
| os.Exit(1) | |
| } | |
| if err := json.Unmarshal(configBytes, &config); err != nil { | |
| fmt.Printf("Failed to parse config file: %s\n", err) | |
| os.Exit(1) | |
| } | |
| headerDescriptions, err := generate(*outputDir, &config) | |
| if err != nil { | |
| fmt.Printf("Failed to generate output: %s\n", err) | |
| os.Exit(1) | |
| } | |
| if err := generateIndex(*outputDir, &config, headerDescriptions); err != nil { | |
| fmt.Printf("Failed to generate index: %s\n", err) | |
| os.Exit(1) | |
| } | |
| if err := copyFile(*outputDir, "doc.css"); err != nil { | |
| fmt.Printf("Failed to copy static file: %s\n", err) | |
| os.Exit(1) | |
| } | |
| } |