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 i := strings.IndexRune(b, '\'')
240 if i < 0 {
241 break
242 }
243 if i+1 >= len(b) || b[i+1] != 's' || (i+2 < len(b) && b[i+2] != ' ') {
244 return ""
245 }
246 b = b[i+2:]
247 }
248
249
250 for b := line; ; {
251 i := strings.IndexRune(b, '.')
252 if i < 0 {
253 break
254 }
255 if i+1 >= len(b) || b[i+1] == ' ' {
256 return ""
257 }
258 b = b[i+1:]
259 }
260
261 return line
262 }
263
264 type op int
265
266 const (
267 opPara op = iota
268 opHead
269 opPre
270 )
271
272 type block struct {
273 op op
274 lines []string
275 }
276
277 var nonAlphaNumRx = lazyregexp.New(`[^a-zA-Z0-9]`)
278
279 func anchorID(line string) string {
280
281 return "hdr-" + nonAlphaNumRx.ReplaceAllString(line, "_")
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 func ToHTML(w io.Writer, text string, words map[string]string) {
307 for _, b := range blocks(text) {
308 switch b.op {
309 case opPara:
310 w.Write(html_p)
311 for _, line := range b.lines {
312 emphasize(w, line, words, true)
313 }
314 w.Write(html_endp)
315 case opHead:
316 w.Write(html_h)
317 id := ""
318 for _, line := range b.lines {
319 if id == "" {
320 id = anchorID(line)
321 w.Write([]byte(id))
322 w.Write(html_hq)
323 }
324 commentEscape(w, line, true)
325 }
326 if id == "" {
327 w.Write(html_hq)
328 }
329 w.Write(html_endh)
330 case opPre:
331 w.Write(html_pre)
332 for _, line := range b.lines {
333 emphasize(w, line, nil, false)
334 }
335 w.Write(html_endpre)
336 }
337 }
338 }
339
340 func blocks(text string) []block {
341 var (
342 out []block
343 para []string
344
345 lastWasBlank = false
346 lastWasHeading = false
347 )
348
349 close := func() {
350 if para != nil {
351 out = append(out, block{opPara, para})
352 para = nil
353 }
354 }
355
356 lines := strings.SplitAfter(text, "\n")
357 unindent(lines)
358 for i := 0; i < len(lines); {
359 line := lines[i]
360 if isBlank(line) {
361
362 close()
363 i++
364 lastWasBlank = true
365 continue
366 }
367 if indentLen(line) > 0 {
368
369 close()
370
371
372 j := i + 1
373 for j < len(lines) && (isBlank(lines[j]) || indentLen(lines[j]) > 0) {
374 j++
375 }
376
377 for j > i && isBlank(lines[j-1]) {
378 j--
379 }
380 pre := lines[i:j]
381 i = j
382
383 unindent(pre)
384
385
386 out = append(out, block{opPre, pre})
387 lastWasHeading = false
388 continue
389 }
390
391 if lastWasBlank && !lastWasHeading && i+2 < len(lines) &&
392 isBlank(lines[i+1]) && !isBlank(lines[i+2]) && indentLen(lines[i+2]) == 0 {
393
394
395
396 if head := heading(line); head != "" {
397 close()
398 out = append(out, block{opHead, []string{head}})
399 i += 2
400 lastWasHeading = true
401 continue
402 }
403 }
404
405
406 lastWasBlank = false
407 lastWasHeading = false
408 para = append(para, lines[i])
409 i++
410 }
411 close()
412
413 return out
414 }
415
416
417
418
419
420 func ToText(w io.Writer, text string, indent, preIndent string, width int) {
421 l := lineWrapper{
422 out: w,
423 width: width,
424 indent: indent,
425 }
426 for _, b := range blocks(text) {
427 switch b.op {
428 case opPara:
429
430 for _, line := range b.lines {
431 line = convertQuotes(line)
432 l.write(line)
433 }
434 l.flush()
435 case opHead:
436 w.Write(nl)
437 for _, line := range b.lines {
438 line = convertQuotes(line)
439 l.write(line + "\n")
440 }
441 l.flush()
442 case opPre:
443 w.Write(nl)
444 for _, line := range b.lines {
445 if isBlank(line) {
446 w.Write([]byte("\n"))
447 } else {
448 w.Write([]byte(preIndent))
449 w.Write([]byte(line))
450 }
451 }
452 }
453 }
454 }
455
456 type lineWrapper struct {
457 out io.Writer
458 printed bool
459 width int
460 indent string
461 n int
462 pendSpace int
463 }
464
465 var nl = []byte("\n")
466 var space = []byte(" ")
467 var prefix = []byte("// ")
468
469 func (l *lineWrapper) write(text string) {
470 if l.n == 0 && l.printed {
471 l.out.Write(nl)
472 }
473 l.printed = true
474
475 needsPrefix := false
476 isComment := strings.HasPrefix(text, "//")
477 for _, f := range strings.Fields(text) {
478 w := utf8.RuneCountInString(f)
479
480 if l.n > 0 && l.n+l.pendSpace+w > l.width {
481 l.out.Write(nl)
482 l.n = 0
483 l.pendSpace = 0
484 needsPrefix = isComment
485 }
486 if l.n == 0 {
487 l.out.Write([]byte(l.indent))
488 }
489 if needsPrefix {
490 l.out.Write(prefix)
491 needsPrefix = false
492 }
493 l.out.Write(space[:l.pendSpace])
494 l.out.Write([]byte(f))
495 l.n += l.pendSpace + w
496 l.pendSpace = 1
497 }
498 }
499
500 func (l *lineWrapper) flush() {
501 if l.n == 0 {
502 return
503 }
504 l.out.Write(nl)
505 l.pendSpace = 0
506 l.n = 0
507 }
508
View as plain text