Source file
src/go/doc/comment.go
1
2
3
4
5
6
7 package doc
8
9 import (
10 "bytes"
11 "internal/lazyregexp"
12 "io"
13 "strings"
14 "text/template"
15 "unicode"
16 "unicode/utf8"
17 )
18
19 const (
20 ldquo = "“"
21 rdquo = "”"
22 ulquo = "“"
23 urquo = "”"
24 )
25
26 var (
27 htmlQuoteReplacer = strings.NewReplacer(ulquo, ldquo, urquo, rdquo)
28 unicodeQuoteReplacer = strings.NewReplacer("``", ulquo, "''", urquo)
29 )
30
31
32
33 func commentEscape(w io.Writer, text string, nice bool) {
34 if nice {
35
36
37 text = convertQuotes(text)
38 var buf bytes.Buffer
39 template.HTMLEscape(&buf, []byte(text))
40
41
42
43 htmlQuoteReplacer.WriteString(w, buf.String())
44 return
45 }
46 template.HTMLEscape(w, []byte(text))
47 }
48
49 func convertQuotes(text string) string {
50 return unicodeQuoteReplacer.Replace(text)
51 }
52
53 const (
54
55 identRx = `[\pL_][\pL_0-9]*`
56
57
58
59
60
61
62
63
64 protoPart = `(https?|ftp|file|gopher|mailto|nntp)`
65
66 hostPart = `([a-zA-Z0-9_@\-.\[\]:]+)`
67
68 pathPart = `([.,:;?!]*[a-zA-Z0-9$'()*+&#=@~_/\-\[\]%])*`
69
70 urlRx = protoPart + `://` + hostPart + pathPart
71 )
72
73 var matchRx = lazyregexp.New(`(` + urlRx + `)|(` + identRx + `)`)
74
75 var (
76 html_a = []byte(`<a href="`)
77 html_aq = []byte(`">`)
78 html_enda = []byte("</a>")
79 html_i = []byte("<i>")
80 html_endi = []byte("</i>")
81 html_p = []byte("<p>\n")
82 html_endp = []byte("</p>\n")
83 html_pre = []byte("<pre>")
84 html_endpre = []byte("</pre>\n")
85 html_h = []byte(`<h3 id="`)
86 html_hq = []byte(`">`)
87 html_endh = []byte("</h3>\n")
88 )
89
90
91
92
93
94
95
96
97
98 func emphasize(w io.Writer, line string, words map[string]string, nice bool) {
99 for {
100 m := matchRx.FindStringSubmatchIndex(line)
101 if m == nil {
102 break
103 }
104
105
106
107 commentEscape(w, line[0:m[0]], nice)
108
109
110 match := line[m[0]:m[1]]
111 if strings.Contains(match, "://") {
112 m0, m1 := m[0], m[1]
113 for _, s := range []string{"()", "{}", "[]"} {
114 open, close := s[:1], s[1:]
115
116 if i := strings.Index(match, close); i >= 0 && i < strings.Index(match, open) {
117 m1 = m0 + i
118 match = line[m0:m1]
119 }
120
121 for i := 0; strings.Count(match, open) != strings.Count(match, close) && i < 10; i++ {
122 m1 = strings.LastIndexAny(line[:m1], s)
123 match = line[m0:m1]
124 }
125 }
126 if m1 != m[1] {
127
128 m = matchRx.FindStringSubmatchIndex(line[:m[0]+len(match)])
129 }
130 }
131
132
133 url := ""
134 italics := false
135 if words != nil {
136 url, italics = words[match]
137 }
138 if m[2] >= 0 {
139
140 if !italics {
141
142 url = match
143 }
144 italics = false
145 }
146
147
148 if len(url) > 0 {
149 w.Write(html_a)
150 template.HTMLEscape(w, []byte(url))
151 w.Write(html_aq)
152 }
153 if italics {
154 w.Write(html_i)
155 }
156 commentEscape(w, match, nice)
157 if italics {
158 w.Write(html_endi)
159 }
160 if len(url) > 0 {
161 w.Write(html_enda)
162 }
163
164
165 line = line[m[1]:]
166 }
167 commentEscape(w, line, nice)
168 }
169
170 func indentLen(s string) int {
171 i := 0
172 for i < len(s) && (s[i] == ' ' || s[i] == '\t') {
173 i++
174 }
175 return i
176 }
177
178 func isBlank(s string) bool {
179 return len(s) == 0 || (len(s) == 1 && s[0] == '\n')
180 }
181
182 func commonPrefix(a, b string) string {
183 i := 0
184 for i < len(a) && i < len(b) && a[i] == b[i] {
185 i++
186 }
187 return a[0:i]
188 }
189
190 func unindent(block []string) {
191 if len(block) == 0 {
192 return
193 }
194
195
196 prefix := block[0][0:indentLen(block[0])]
197 for _, line := range block {
198 if !isBlank(line) {
199 prefix = commonPrefix(prefix, line[0:indentLen(line)])
200 }
201 }
202 n := len(prefix)
203
204
205 for i, line := range block {
206 if !isBlank(line) {
207 block[i] = line[n:]
208 }
209 }
210 }
211
212
213
214 func heading(line string) string {
215 line = strings.TrimSpace(line)
216 if len(line) == 0 {
217 return ""
218 }
219
220
221 r, _ := utf8.DecodeRuneInString(line)
222 if !unicode.IsLetter(r) || !unicode.IsUpper(r) {
223 return ""
224 }
225
226
227 r, _ = utf8.DecodeLastRuneInString(line)
228 if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
229 return ""
230 }
231
232
233 if strings.ContainsAny(line, ";:!?+*/=[]{}_^°&§~%#@<\">\\") {
234 return ""
235 }
236
237
238 for b := line; ; {
239 var ok bool
240 if _, b, ok = strings.Cut(b, "'"); !ok {
241 break
242 }
243 if b != "s" && !strings.HasPrefix(b, "s ") {
244 return ""
245 }
246 }
247
248
249 for b := line; ; {
250 var ok bool
251 if _, b, ok = strings.Cut(b, "."); !ok {
252 break
253 }
254 if b == "" || strings.HasPrefix(b, " ") {
255 return ""
256 }
257 }
258
259 return line
260 }
261
262 type op int
263
264 const (
265 opPara op = iota
266 opHead
267 opPre
268 )
269
270 type block struct {
271 op op
272 lines []string
273 }
274
275 var nonAlphaNumRx = lazyregexp.New(`[^a-zA-Z0-9]`)
276
277 func anchorID(line string) string {
278
279 return "hdr-" + nonAlphaNumRx.ReplaceAllString(line, "_")
280 }
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307 func ToHTML(w io.Writer, text string, words map[string]string) {
308 for _, b := range blocks(text) {
309 switch b.op {
310 case opPara:
311 w.Write(html_p)
312 for _, line := range b.lines {
313 emphasize(w, line, words, true)
314 }
315 w.Write(html_endp)
316 case opHead:
317 w.Write(html_h)
318 id := ""
319 for _, line := range b.lines {
320 if id == "" {
321 id = anchorID(line)
322 w.Write([]byte(id))
323 w.Write(html_hq)
324 }
325 commentEscape(w, line, true)
326 }
327 if id == "" {
328 w.Write(html_hq)
329 }
330 w.Write(html_endh)
331 case opPre:
332 w.Write(html_pre)
333 for _, line := range b.lines {
334 emphasize(w, line, nil, false)
335 }
336 w.Write(html_endpre)
337 }
338 }
339 }
340
341 func blocks(text string) []block {
342 var (
343 out []block
344 para []string
345
346 lastWasBlank = false
347 lastWasHeading = false
348 )
349
350 close := func() {
351 if para != nil {
352 out = append(out, block{opPara, para})
353 para = nil
354 }
355 }
356
357 lines := strings.SplitAfter(text, "\n")
358 unindent(lines)
359 for i := 0; i < len(lines); {
360 line := lines[i]
361 if isBlank(line) {
362
363 close()
364 i++
365 lastWasBlank = true
366 continue
367 }
368 if indentLen(line) > 0 {
369
370 close()
371
372
373 j := i + 1
374 for j < len(lines) && (isBlank(lines[j]) || indentLen(lines[j]) > 0) {
375 j++
376 }
377
378 for j > i && isBlank(lines[j-1]) {
379 j--
380 }
381 pre := lines[i:j]
382 i = j
383
384 unindent(pre)
385
386
387 out = append(out, block{opPre, pre})
388 lastWasHeading = false
389 continue
390 }
391
392 if lastWasBlank && !lastWasHeading && i+2 < len(lines) &&
393 isBlank(lines[i+1]) && !isBlank(lines[i+2]) && indentLen(lines[i+2]) == 0 {
394
395
396
397 if head := heading(line); head != "" {
398 close()
399 out = append(out, block{opHead, []string{head}})
400 i += 2
401 lastWasHeading = true
402 continue
403 }
404 }
405
406
407 lastWasBlank = false
408 lastWasHeading = false
409 para = append(para, lines[i])
410 i++
411 }
412 close()
413
414 return out
415 }
416
417
418
419
420
421
422
423
424 func ToText(w io.Writer, text string, indent, preIndent string, width int) {
425 l := lineWrapper{
426 out: w,
427 width: width,
428 indent: indent,
429 }
430 for _, b := range blocks(text) {
431 switch b.op {
432 case opPara:
433
434 for _, line := range b.lines {
435 line = convertQuotes(line)
436 l.write(line)
437 }
438 l.flush()
439 case opHead:
440 w.Write(nl)
441 for _, line := range b.lines {
442 line = convertQuotes(line)
443 l.write(line + "\n")
444 }
445 l.flush()
446 case opPre:
447 w.Write(nl)
448 for _, line := range b.lines {
449 if isBlank(line) {
450 w.Write([]byte("\n"))
451 } else {
452 w.Write([]byte(preIndent))
453 w.Write([]byte(line))
454 }
455 }
456 }
457 }
458 }
459
460 type lineWrapper struct {
461 out io.Writer
462 printed bool
463 width int
464 indent string
465 n int
466 pendSpace int
467 }
468
469 var nl = []byte("\n")
470 var space = []byte(" ")
471 var prefix = []byte("// ")
472
473 func (l *lineWrapper) write(text string) {
474 if l.n == 0 && l.printed {
475 l.out.Write(nl)
476 }
477 l.printed = true
478
479 needsPrefix := false
480 isComment := strings.HasPrefix(text, "//")
481 for _, f := range strings.Fields(text) {
482 w := utf8.RuneCountInString(f)
483
484 if l.n > 0 && l.n+l.pendSpace+w > l.width {
485 l.out.Write(nl)
486 l.n = 0
487 l.pendSpace = 0
488 needsPrefix = isComment && !strings.HasPrefix(f, "//")
489 }
490 if l.n == 0 {
491 l.out.Write([]byte(l.indent))
492 }
493 if needsPrefix {
494 l.out.Write(prefix)
495 needsPrefix = false
496 }
497 l.out.Write(space[:l.pendSpace])
498 l.out.Write([]byte(f))
499 l.n += l.pendSpace + w
500 l.pendSpace = 1
501 }
502 }
503
504 func (l *lineWrapper) flush() {
505 if l.n == 0 {
506 return
507 }
508 l.out.Write(nl)
509 l.pendSpace = 0
510 l.n = 0
511 }
512
View as plain text