1
2
3
4
5
6
7 package doc
8
9 import (
10 "go/ast"
11 "io"
12 "regexp"
13 "strings"
14 "template"
15 )
16
17 func isWhitespace(ch byte) bool { return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' }
18
19 func stripTrailingWhitespace(s string) string {
20 i := len(s)
21 for i > 0 && isWhitespace(s[i-1]) {
22 i--
23 }
24 return s[0:i]
25 }
26
27
28
29 func CommentText(comment *ast.CommentGroup) string {
30 if comment == nil {
31 return ""
32 }
33 comments := make([]string, len(comment.List))
34 for i, c := range comment.List {
35 comments[i] = string(c.Text)
36 }
37
38 lines := make([]string, 0, 10)
39 for _, c := range comments {
40
41
42 switch c[1] {
43 case '/':
44
45 c = c[2:]
46
47
48
49 if len(c) > 0 && c[0] == ' ' {
50 c = c[1:]
51 }
52 case '*':
53
54 c = c[2 : len(c)-2]
55 }
56
57
58 cl := strings.Split(c, "\n")
59
60
61 for _, l := range cl {
62 lines = append(lines, stripTrailingWhitespace(l))
63 }
64 }
65
66
67
68 n := 0
69 for _, line := range lines {
70 if line != "" || n > 0 && lines[n-1] != "" {
71 lines[n] = line
72 n++
73 }
74 }
75 lines = lines[0:n]
76
77
78 if n > 0 && lines[n-1] != "" {
79 lines = append(lines, "")
80 }
81
82 return strings.Join(lines, "\n")
83 }
84
85
86 func split(text []byte) [][]byte {
87
88 n := 0
89 last := 0
90 for i, c := range text {
91 if c == '\n' {
92 last = i + 1
93 n++
94 }
95 }
96 if last < len(text) {
97 n++
98 }
99
100
101 out := make([][]byte, n)
102 last = 0
103 n = 0
104 for i, c := range text {
105 if c == '\n' {
106 out[n] = text[last : i+1]
107 last = i + 1
108 n++
109 }
110 }
111 if last < len(text) {
112 out[n] = text[last:]
113 }
114
115 return out
116 }
117
118 var (
119 ldquo = []byte("“")
120 rdquo = []byte("”")
121 )
122
123
124
125 func commentEscape(w io.Writer, s []byte, nice bool) {
126 last := 0
127 if nice {
128 for i := 0; i < len(s)-1; i++ {
129 ch := s[i]
130 if ch == s[i+1] && (ch == '`' || ch == '\'') {
131 template.HTMLEscape(w, s[last:i])
132 last = i + 2
133 switch ch {
134 case '`':
135 w.Write(ldquo)
136 case '\'':
137 w.Write(rdquo)
138 }
139 i++
140 }
141 }
142 }
143 template.HTMLEscape(w, s[last:])
144 }
145
146 const (
147
148 identRx = `[a-zA-Z_][a-zA-Z_0-9]*`
149
150
151 protocol = `(https?|ftp|file|gopher|mailto|news|nntp|telnet|wais|prospero):`
152 hostPart = `[a-zA-Z0-9_@\-]+`
153 filePart = `[a-zA-Z0-9_?%#~&/\-+=]+`
154 urlRx = protocol + `//` +
155 hostPart + `([.:]` + hostPart + `)*/?` +
156 filePart + `([:.,]` + filePart + `)*`
157 )
158
159 var matchRx = regexp.MustCompile(`(` + identRx + `)|(` + urlRx + `)`)
160
161 var (
162 html_a = []byte(`<a href="`)
163 html_aq = []byte(`">`)
164 html_enda = []byte("</a>")
165 html_i = []byte("<i>")
166 html_endi = []byte("</i>")
167 html_p = []byte("<p>\n")
168 html_endp = []byte("</p>\n")
169 html_pre = []byte("<pre>")
170 html_endpre = []byte("</pre>\n")
171 )
172
173
174
175
176
177
178
179
180
181 func emphasize(w io.Writer, line []byte, words map[string]string, nice bool) {
182 for {
183 m := matchRx.FindSubmatchIndex(line)
184 if m == nil {
185 break
186 }
187
188
189
190 commentEscape(w, line[0:m[0]], nice)
191
192
193 match := line[m[0]:m[1]]
194 url := ""
195 italics := false
196 if words != nil {
197 url, italics = words[string(match)]
198 }
199 if m[2] < 0 {
200
201 if !italics {
202
203 url = string(match)
204 }
205 italics = false
206 }
207
208
209 if len(url) > 0 {
210 w.Write(html_a)
211 template.HTMLEscape(w, []byte(url))
212 w.Write(html_aq)
213 }
214 if italics {
215 w.Write(html_i)
216 }
217 commentEscape(w, match, nice)
218 if italics {
219 w.Write(html_endi)
220 }
221 if len(url) > 0 {
222 w.Write(html_enda)
223 }
224
225
226 line = line[m[1]:]
227 }
228 commentEscape(w, line, nice)
229 }
230
231 func indentLen(s []byte) int {
232 i := 0
233 for i < len(s) && (s[i] == ' ' || s[i] == '\t') {
234 i++
235 }
236 return i
237 }
238
239 func isBlank(s []byte) bool { return len(s) == 0 || (len(s) == 1 && s[0] == '\n') }
240
241 func commonPrefix(a, b []byte) []byte {
242 i := 0
243 for i < len(a) && i < len(b) && a[i] == b[i] {
244 i++
245 }
246 return a[0:i]
247 }
248
249 func unindent(block [][]byte) {
250 if len(block) == 0 {
251 return
252 }
253
254
255 prefix := block[0][0:indentLen(block[0])]
256 for _, line := range block {
257 if !isBlank(line) {
258 prefix = commonPrefix(prefix, line[0:indentLen(line)])
259 }
260 }
261 n := len(prefix)
262
263
264 for i, line := range block {
265 if !isBlank(line) {
266 block[i] = line[n:]
267 }
268 }
269 }
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287 func ToHTML(w io.Writer, s []byte, words map[string]string) {
288 inpara := false
289
290 close := func() {
291 if inpara {
292 w.Write(html_endp)
293 inpara = false
294 }
295 }
296 open := func() {
297 if !inpara {
298 w.Write(html_p)
299 inpara = true
300 }
301 }
302
303 lines := split(s)
304 unindent(lines)
305 for i := 0; i < len(lines); {
306 line := lines[i]
307 if isBlank(line) {
308
309 close()
310 i++
311 continue
312 }
313 if indentLen(line) > 0 {
314
315 close()
316
317
318 j := i + 1
319 for j < len(lines) && (isBlank(lines[j]) || indentLen(lines[j]) > 0) {
320 j++
321 }
322
323 for j > i && isBlank(lines[j-1]) {
324 j--
325 }
326 block := lines[i:j]
327 i = j
328
329 unindent(block)
330
331
332 w.Write(html_pre)
333 for _, line := range block {
334 emphasize(w, line, nil, false)
335 }
336 w.Write(html_endpre)
337 continue
338 }
339
340 open()
341 emphasize(w, lines[i], words, true)
342 i++
343 }
344 close()
345 }