blob: 1de7064bba28bb8798763048117f7af82c09aee4 [file] [log] [blame]
David Benjaminece1f862023-04-24 16:14:08 -04001//go:build ignore
2
Adam Langley95c29f32014-06-20 12:00:00 -07003// 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.
Adam Langley95c29f32014-06-20 12:00:00 -07009package main
10
11import (
12 "bufio"
13 "encoding/json"
14 "errors"
15 "flag"
16 "fmt"
17 "html/template"
Adam Langley95c29f32014-06-20 12:00:00 -070018 "os"
19 "path/filepath"
David Benjamin855dabc2018-04-25 17:34:24 -040020 "regexp"
David Benjamina942d572023-12-14 14:27:27 -050021 "strconv"
Adam Langley95c29f32014-06-20 12:00:00 -070022 "strings"
David Benjamina942d572023-12-14 14:27:27 -050023 "unicode"
Adam Langley95c29f32014-06-20 12:00:00 -070024)
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.
David Benjamina942d572023-12-14 14:27:27 -050046 Preamble []CommentBlock
Adam Langley95c29f32014-06-20 12:00:00 -070047 Sections []HeaderSection
David Benjamindfa9c4a2015-10-18 01:08:11 -040048 // AllDecls maps all decls to their URL fragments.
49 AllDecls map[string]string
Adam Langley95c29f32014-06-20 12:00:00 -070050}
51
52type HeaderSection struct {
53 // Preamble contains a comment for a group of functions.
David Benjamina942d572023-12-14 14:27:27 -050054 Preamble []CommentBlock
Adam Langley95c29f32014-06-20 12:00:00 -070055 Decls []HeaderDecl
David Benjamin1bfce802015-09-07 13:21:08 -040056 // Anchor, if non-empty, is the URL fragment to use in anchor tags.
57 Anchor string
Adam Langley95c29f32014-06-20 12:00:00 -070058 // 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.
David Benjamina942d572023-12-14 14:27:27 -050067 Comment []CommentBlock
Adam Langley95c29f32014-06-20 12:00:00 -070068 // 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
David Benjamin1bfce802015-09-07 13:21:08 -040072 // Anchor, if non-empty, is the URL fragment to use in anchor tags.
73 Anchor string
Adam Langley95c29f32014-06-20 12:00:00 -070074}
75
David Benjamina942d572023-12-14 14:27:27 -050076type 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
Adam Langley95c29f32014-06-20 12:00:00 -070090const (
91 cppGuard = "#if defined(__cplusplus)"
92 commentStart = "/* "
93 commentEnd = " */"
David Benjaminef37ab52017-08-03 01:07:05 -040094 lineComment = "// "
Adam Langley95c29f32014-06-20 12:00:00 -070095)
96
David Benjaminef37ab52017-08-03 01:07:05 -040097func isComment(line string) bool {
98 return strings.HasPrefix(line, commentStart) || strings.HasPrefix(line, lineComment)
99}
100
David Benjamin92812cb2018-09-03 16:20:09 -0500101func 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
David Benjamina942d572023-12-14 14:27:27 -0500114func extractCommentLines(lines []string, lineNo int) (comment []string, rest []string, restLineNo int, err error) {
Adam Langley95c29f32014-06-20 12:00:00 -0700115 if len(lines) == 0 {
116 return nil, lines, lineNo, nil
117 }
118
119 restLineNo = lineNo
120 rest = lines
121
David Benjaminef37ab52017-08-03 01:07:05 -0400122 var isBlock bool
123 if strings.HasPrefix(rest[0], commentStart) {
124 isBlock = true
125 } else if !strings.HasPrefix(rest[0], lineComment) {
Adam Langley95c29f32014-06-20 12:00:00 -0700126 panic("extractComment called on non-comment")
127 }
David Benjamina942d572023-12-14 14:27:27 -0500128 comment = []string{rest[0][len(commentStart):]}
Adam Langley95c29f32014-06-20 12:00:00 -0700129 rest = rest[1:]
130 restLineNo++
131
132 for len(rest) > 0 {
David Benjaminef37ab52017-08-03 01:07:05 -0400133 if isBlock {
David Benjamina942d572023-12-14 14:27:27 -0500134 last := &comment[len(comment)-1]
135 if i := strings.Index(*last, commentEnd); i >= 0 {
136 if i != len(*last)-len(commentEnd) {
David Benjaminef37ab52017-08-03 01:07:05 -0400137 err = fmt.Errorf("garbage after comment end on line %d", restLineNo)
138 return
139 }
David Benjamina942d572023-12-14 14:27:27 -0500140 *last = (*last)[:i]
Adam Langley95c29f32014-06-20 12:00:00 -0700141 return
142 }
David Benjaminef37ab52017-08-03 01:07:05 -0400143 }
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, "//") {
Adam Langley95c29f32014-06-20 12:00:00 -0700152 return
153 }
David Benjamina942d572023-12-14 14:27:27 -0500154 comment = append(comment, line[2:])
Adam Langley95c29f32014-06-20 12:00:00 -0700155 rest = rest[1:]
156 restLineNo++
157 }
158
159 err = errors.New("hit EOF in comment")
160 return
161}
162
David Benjamina942d572023-12-14 14:27:27 -0500163func 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
Adam Langley95c29f32014-06-20 12:00:00 -0700285func extractDecl(lines []string, lineNo int) (decl string, rest []string, restLineNo int, err error) {
David Benjamin0a211df2016-12-17 15:25:55 -0500286 if len(lines) == 0 || len(lines[0]) == 0 {
Adam Langley95c29f32014-06-20 12:00:00 -0700287 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
David Benjamin71485af2015-04-09 00:06:03 -0400338func skipLine(s string) string {
339 i := strings.Index(s, "\n")
340 if i > 0 {
341 return s[i:]
342 }
343 return ""
344}
345
David Benjamin855dabc2018-04-25 17:34:24 -0400346var stackOfRegexp = regexp.MustCompile(`STACK_OF\(([^)]*)\)`)
347var lhashOfRegexp = regexp.MustCompile(`LHASH_OF\(([^)]*)\)`)
348
Adam Langley95c29f32014-06-20 12:00:00 -0700349func getNameFromDecl(decl string) (string, bool) {
David Benjaminb4804282015-05-16 12:12:31 -0400350 for strings.HasPrefix(decl, "#if") || strings.HasPrefix(decl, "#elif") {
David Benjamin71485af2015-04-09 00:06:03 -0400351 decl = skipLine(decl)
352 }
Adam Langleyeac0ce02016-01-25 14:26:05 -0800353
354 if strings.HasPrefix(decl, "typedef ") {
Adam Langley95c29f32014-06-20 12:00:00 -0700355 return "", false
356 }
Adam Langleyeac0ce02016-01-25 14:26:05 -0800357
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
David Benjamin6deacb32015-05-16 12:00:51 -0400365 for len(decl) > 0 && decl[0] == ' ' {
366 decl = decl[1:]
367 }
Adam Langleyeac0ce02016-01-25 14:26:05 -0800368
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
David Benjamin6deacb32015-05-16 12:00:51 -0400375 i := strings.IndexAny(decl, "( ")
376 if i < 0 {
377 return "", false
378 }
379 return decl[:i], true
380 }
David Benjamin361ecc02015-09-13 01:16:50 -0400381 decl = strings.TrimPrefix(decl, "OPENSSL_EXPORT ")
David Benjamin855dabc2018-04-25 17:34:24 -0400382 decl = strings.TrimPrefix(decl, "const ")
383 decl = stackOfRegexp.ReplaceAllString(decl, "STACK_OF_$1")
384 decl = lhashOfRegexp.ReplaceAllString(decl, "LHASH_OF_$1")
Adam Langley95c29f32014-06-20 12:00:00 -0700385 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
David Benjamin1bfce802015-09-07 13:21:08 -0400399func sanitizeAnchor(name string) string {
400 return strings.Replace(name, " ", "-", -1)
401}
402
David Benjamin5ef619e2015-10-18 00:10:28 -0400403func isPrivateSection(name string) bool {
404 return strings.HasPrefix(name, "Private functions") || strings.HasPrefix(name, "Private structures") || strings.Contains(name, "(hidden)")
405}
406
David Benjamin1d842c62021-05-05 15:46:15 -0400407func isCollectiveComment(line string) bool {
408 return strings.HasPrefix(line, "The ") || strings.HasPrefix(line, "These ")
409}
410
Adam Langley95c29f32014-06-20 12:00:00 -0700411func (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
David Benjamin68a533c2016-05-17 17:36:47 -0400429 lineNo := 1
Adam Langley95c29f32014-06-20 12:00:00 -0700430 found := false
431 for i, line := range lines {
Adam Langley95c29f32014-06-20 12:00:00 -0700432 if line == cppGuard {
433 lines = lines[i+1:]
David Benjamin68a533c2016-05-17 17:36:47 -0400434 lineNo += i + 1
Adam Langley95c29f32014-06-20 12:00:00 -0700435 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 }
Adam Langley10f97f32016-07-12 08:09:33 -0700447 lineNo += 2
448 lines = lines[2:]
Adam Langley95c29f32014-06-20 12:00:00 -0700449
450 header := &HeaderFile{
David Benjamindfa9c4a2015-10-18 01:08:11 -0400451 Name: filepath.Base(path),
452 AllDecls: make(map[string]string),
Adam Langley95c29f32014-06-20 12:00:00 -0700453 }
454
455 for i, line := range lines {
Adam Langley95c29f32014-06-20 12:00:00 -0700456 if len(line) > 0 {
457 lines = lines[i:]
David Benjamin68a533c2016-05-17 17:36:47 -0400458 lineNo += i
Adam Langley95c29f32014-06-20 12:00:00 -0700459 break
460 }
461 }
462
463 oldLines = lines
David Benjaminef37ab52017-08-03 01:07:05 -0400464 if len(lines) > 0 && isComment(lines[0]) {
Adam Langley95c29f32014-06-20 12:00:00 -0700465 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
David Benjamin1bfce802015-09-07 13:21:08 -0400482 allAnchors := make(map[string]struct{})
Adam Langley95c29f32014-06-20 12:00:00 -0700483
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
David Benjamin1bfce802015-09-07 13:21:08 -0400498 var section HeaderSection
Adam Langley95c29f32014-06-20 12:00:00 -0700499
David Benjaminef37ab52017-08-03 01:07:05 -0400500 if isComment(line) {
Adam Langley95c29f32014-06-20 12:00:00 -0700501 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 {
David Benjamina942d572023-12-14 14:27:27 -0500506 heading := firstSentence(comment)
507 anchor := sanitizeAnchor(heading)
David Benjamin1bfce802015-09-07 13:21:08 -0400508 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
Adam Langley95c29f32014-06-20 12:00:00 -0700515 section.Preamble = comment
David Benjamina942d572023-12-14 14:27:27 -0500516 section.IsPrivate = isPrivateSection(heading)
David Benjamin1bfce802015-09-07 13:21:08 -0400517 section.Anchor = anchor
Adam Langley95c29f32014-06-20 12:00:00 -0700518 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
David Benjamin54b04fd2023-01-29 11:56:25 -0500529 }
Adam Langley95c29f32014-06-20 12:00:00 -0700530 if line == cppGuard {
Bob Beck2fd8de62022-11-23 10:13:23 -0700531 return nil, fmt.Errorf("hit ending C++ guard while in section on line %d (possibly missing two empty lines ahead of guard?)", lineNo)
Adam Langley95c29f32014-06-20 12:00:00 -0700532 }
533
David Benjamina942d572023-12-14 14:27:27 -0500534 var comment []CommentBlock
Adam Langley95c29f32014-06-20 12:00:00 -0700535 var decl string
David Benjaminef37ab52017-08-03 01:07:05 -0400536 if isComment(line) {
Adam Langley95c29f32014-06-20 12:00:00 -0700537 comment, lines, lineNo, err = extractComment(lines, lineNo)
538 if err != nil {
539 return nil, err
540 }
541 }
542 if len(lines) == 0 {
Daniel McArdle1a635072020-07-20 12:30:05 -0400543 return nil, fmt.Errorf("expected decl at EOF on line %d", lineNo)
Adam Langley95c29f32014-06-20 12:00:00 -0700544 }
David Benjamin68a533c2016-05-17 17:36:47 -0400545 declLineNo := lineNo
Adam Langley95c29f32014-06-20 12:00:00 -0700546 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 {
Adam Langley5f889992015-11-04 14:05:00 -0800557 // 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
David Benjamin1d842c62021-05-05 15:46:15 -0400560 // collective comments.
David Benjamina942d572023-12-14 14:27:27 -0500561 sentence := firstSentence(comment)
Adam Langley5f889992015-11-04 14:05:00 -0800562 if len(comment) > 0 &&
David Benjamin92812cb2018-09-03 16:20:09 -0500563 len(name) > 0 &&
David Benjamina942d572023-12-14 14:27:27 -0500564 !isCollectiveComment(sentence) {
565 subject := commentSubject(sentence)
David Benjamin92812cb2018-09-03 16:20:09 -0500566 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 {
David Benjamin3f18c4c2018-09-14 13:36:12 -0700573 return nil, fmt.Errorf("comment for %q doesn't seem to match line %s:%d\n", name, path, declLineNo)
David Benjamin92812cb2018-09-03 16:20:09 -0500574 }
Adam Langley5f889992015-11-04 14:05:00 -0800575 }
David Benjamin1bfce802015-09-07 13:21:08 -0400576 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
David Benjamindfa9c4a2015-10-18 01:08:11 -0400582 header.AllDecls[name] = anchor
583
Adam Langley95c29f32014-06-20 12:00:00 -0700584 section.Decls = append(section.Decls, HeaderDecl{
585 Comment: comment,
586 Name: name,
587 Decl: decl,
David Benjamin1bfce802015-09-07 13:21:08 -0400588 Anchor: anchor,
Adam Langley95c29f32014-06-20 12:00:00 -0700589 })
Adam Langley95c29f32014-06-20 12:00:00 -0700590 }
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
David Benjamina942d572023-12-14 14:27:27 -0500604func firstSentence(comment []CommentBlock) string {
605 if len(comment) == 0 {
Adam Langley95c29f32014-06-20 12:00:00 -0700606 return ""
607 }
David Benjamina942d572023-12-14 14:27:27 -0500608 s := comment[0].Paragraph
Adam Langley95c29f32014-06-20 12:00:00 -0700609 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
David Benjamina942d572023-12-14 14:27:27 -0500619func 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:
David Benjamin89dd8d92023-12-16 10:47:35 -0500644 if strings.HasPrefix(block.Paragraph, "WARNING:") {
645 b.WriteString("<p class=\"warning\">")
646 } else {
647 b.WriteString("<p>")
648 }
David Benjamina942d572023-12-14 14:27:27 -0500649 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
David Benjamind8ea3902017-08-04 19:08:44 -0400678// markupPipeWords converts |s| into an HTML string, safe to be included outside
679// a tag, while also marking up words surrounded by |.
David Benjamin81502be2022-02-25 19:47:16 -0500680func markupPipeWords(allDecls map[string]string, s string, linkDecls bool) template.HTML {
David Benjamind8ea3902017-08-04 19:08:44 -0400681 // 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)
Adam Langley95c29f32014-06-20 12:00:00 -0700685 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>"
David Benjamindfa9c4a2015-10-18 01:08:11 -0400700 anchor, isLink := allDecls[s[:i]]
David Benjamin81502be2022-02-25 19:47:16 -0500701 if linkDecls && isLink {
702 ret += fmt.Sprintf("<a href=\"%s\">%s</a>", template.HTMLEscapeString(anchor), s[:i])
703 } else {
704 ret += s[:i]
David Benjamindfa9c4a2015-10-18 01:08:11 -0400705 }
Adam Langley95c29f32014-06-20 12:00:00 -0700706 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 {
David Benjamin1d842c62021-05-05 15:46:15 -0400717 if isCollectiveComment(string(s)) {
718 return s
719 }
David Benjamin5b082e82014-12-26 00:54:52 -0500720 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]))
David Benjamindfa9c4a2015-10-18 01:08:11 -0400726 // 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 }
David Benjamin7e40d4e2015-09-07 13:17:45 -0400731 if w == "a" || w == "an" {
David Benjamin5b082e82014-12-26 00:54:52 -0500732 start = end + 1
733 goto again
734 }
735 return s[:start] + "<span class=\"first-word\">" + s[start:end] + "</span>" + s[end:]
Adam Langley95c29f32014-06-20 12:00:00 -0700736 }
737 return s
738}
739
David Benjamin047ff642021-08-19 18:17:44 -0400740var 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
Adam Langley95c29f32014-06-20 12:00:00 -0700762func generate(outPath string, config *Config) (map[string]string, error) {
David Benjamindfa9c4a2015-10-18 01:08:11 -0400763 allDecls := make(map[string]string)
764
Adam Langley95c29f32014-06-20 12:00:00 -0700765 headerTmpl := template.New("headerTmpl")
766 headerTmpl.Funcs(template.FuncMap{
David Benjamin81502be2022-02-25 19:47:16 -0500767 "firstSentence": firstSentence,
David Benjamin81502be2022-02-25 19:47:16 -0500768 "markupPipeWordsNoLink": func(s string) template.HTML { return markupPipeWords(allDecls, s, false /* linkDecls */) },
David Benjamina942d572023-12-14 14:27:27 -0500769 "markupComment": func(c []CommentBlock) template.HTML { return markupComment(allDecls, c) },
Adam Langley95c29f32014-06-20 12:00:00 -0700770 })
David Benjamin5b082e82014-12-26 00:54:52 -0500771 headerTmpl, err := headerTmpl.Parse(`<!DOCTYPE html>
Adam Langley95c29f32014-06-20 12:00:00 -0700772<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">
David Benjamin2b1ca802016-05-20 11:28:59 -0400781 <div class="title">
782 <h2>{{.Name}}</h2>
783 <a href="headers.html">All headers</a>
784 </div>
Adam Langley95c29f32014-06-20 12:00:00 -0700785
David Benjamina942d572023-12-14 14:27:27 -0500786 {{if .Preamble}}<div class="comment">{{.Preamble | markupComment}}</div>{{end}}
Adam Langley95c29f32014-06-20 12:00:00 -0700787
David Benjamina942d572023-12-14 14:27:27 -0500788 <ol class="toc">
Adam Langley95c29f32014-06-20 12:00:00 -0700789 {{range .Sections}}
790 {{if not .IsPrivate}}
David Benjamin81502be2022-02-25 19:47:16 -0500791 {{if .Anchor}}<li class="header"><a href="#{{.Anchor}}">{{.Preamble | firstSentence | markupPipeWordsNoLink}}</a></li>{{end}}
Adam Langley95c29f32014-06-20 12:00:00 -0700792 {{range .Decls}}
David Benjamin1bfce802015-09-07 13:21:08 -0400793 {{if .Anchor}}<li><a href="#{{.Anchor}}"><tt>{{.Name}}</tt></a></li>{{end}}
Adam Langley95c29f32014-06-20 12:00:00 -0700794 {{end}}
795 {{end}}
796 {{end}}
797 </ol>
798
799 {{range .Sections}}
800 {{if not .IsPrivate}}
David Benjamindfa9c4a2015-10-18 01:08:11 -0400801 <div class="section" {{if .Anchor}}id="{{.Anchor}}"{{end}}>
David Benjamina942d572023-12-14 14:27:27 -0500802 {{if .Preamble}}<div class="sectionpreamble comment">{{.Preamble | markupComment}}</div>{{end}}
Adam Langley95c29f32014-06-20 12:00:00 -0700803
804 {{range .Decls}}
David Benjamindfa9c4a2015-10-18 01:08:11 -0400805 <div class="decl" {{if .Anchor}}id="{{.Anchor}}"{{end}}>
David Benjamina942d572023-12-14 14:27:27 -0500806 {{if .Comment}}<div class="comment">{{.Comment | markupComment}}</div>{{end}}
807 {{if .Decl}}<pre class="code">{{.Decl}}</pre>{{end}}
Adam Langley95c29f32014-06-20 12:00:00 -0700808 </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)
David Benjamindfa9c4a2015-10-18 01:08:11 -0400821 var headers []*HeaderFile
Adam Langley95c29f32014-06-20 12:00:00 -0700822
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)
David Benjamindfa9c4a2015-10-18 01:08:11 -0400830 headers = append(headers, header)
831
832 for name, anchor := range header.AllDecls {
833 allDecls[name] = fmt.Sprintf("%s#%s", header.Name+".html", anchor)
Adam Langley95c29f32014-06-20 12:00:00 -0700834 }
David Benjamindfa9c4a2015-10-18 01:08:11 -0400835 }
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
Adam Langley95c29f32014-06-20 12:00:00 -0700847 }
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">
David Benjamin2b1ca802016-05-20 11:28:59 -0400871 <div class="title">
872 <h2>BoringSSL Headers</h2>
873 </div>
Adam Langley95c29f32014-06-20 12:00:00 -0700874 <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
Brian Smith55a3cf42015-08-09 17:08:49 -0400903func copyFile(outPath string, inFilePath string) error {
David Benjamin5511fa82022-11-12 15:52:28 +0000904 bytes, err := os.ReadFile(inFilePath)
Brian Smith55a3cf42015-08-09 17:08:49 -0400905 if err != nil {
906 return err
907 }
David Benjamin5511fa82022-11-12 15:52:28 +0000908 return os.WriteFile(filepath.Join(outPath, filepath.Base(inFilePath)), bytes, 0666)
Brian Smith55a3cf42015-08-09 17:08:49 -0400909}
910
Adam Langley95c29f32014-06-20 12:00:00 -0700911func main() {
912 var (
Adam Langley0fd56392015-04-08 17:32:55 -0700913 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")
Adam Langley95c29f32014-06-20 12:00:00 -0700915 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
David Benjamin5511fa82022-11-12 15:52:28 +0000930 configBytes, err := os.ReadFile(*configFlag)
Adam Langley95c29f32014-06-20 12:00:00 -0700931 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 }
Brian Smith55a3cf42015-08-09 17:08:49 -0400951
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 }
Adam Langley95c29f32014-06-20 12:00:00 -0700956}