Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 1 | // Copyright 2013 The Go Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style |
| 3 | // license that can be found in the LICENSE file. |
| 4 | |
| 5 | // Package godoc is a work-in-progress (2013-07-17) package to |
| 6 | // begin splitting up the godoc binary into multiple pieces. |
| 7 | // |
| 8 | // This package comment will evolve over time as this package splits |
| 9 | // into smaller pieces. |
David Symonds | 24257c8 | 2014-12-09 15:00:58 +1100 | [diff] [blame] | 10 | package godoc // import "golang.org/x/tools/godoc" |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 11 | |
| 12 | import ( |
Brad Fitzpatrick | 32950ab | 2018-07-17 21:23:18 +0000 | [diff] [blame] | 13 | "bufio" |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 14 | "bytes" |
| 15 | "fmt" |
| 16 | "go/ast" |
| 17 | "go/doc" |
| 18 | "go/format" |
| 19 | "go/printer" |
| 20 | "go/token" |
Alan Donovan | 80c4f06 | 2014-03-14 18:58:22 -0400 | [diff] [blame] | 21 | htmltemplate "html/template" |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 22 | "io" |
| 23 | "log" |
| 24 | "os" |
| 25 | pathpkg "path" |
| 26 | "regexp" |
Brad Garcia | ff7cfaf | 2013-12-04 10:37:01 -0500 | [diff] [blame] | 27 | "strconv" |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 28 | "strings" |
| 29 | "text/template" |
| 30 | "time" |
| 31 | "unicode" |
| 32 | "unicode/utf8" |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 33 | ) |
| 34 | |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 35 | // Fake relative package path for built-ins. Documentation for all globals |
Dmitri Shuralyov | 9394956 | 2019-02-18 13:49:49 -0500 | [diff] [blame] | 36 | // (not just exported ones) will be shown for packages in this directory, |
| 37 | // and there will be no association of consts, vars, and factory functions |
| 38 | // with types (see issue 6645). |
Brad Fitzpatrick | 705bb7f | 2013-07-19 10:27:53 +1000 | [diff] [blame] | 39 | const builtinPkgPath = "builtin" |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 40 | |
| 41 | // FuncMap defines template functions used in godoc templates. |
| 42 | // |
| 43 | // Convention: template function names ending in "_html" or "_url" produce |
| 44 | // HTML- or URL-escaped strings; all other function results may |
| 45 | // require explicit escaping in the template. |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 46 | func (p *Presentation) FuncMap() template.FuncMap { |
| 47 | p.initFuncMapOnce.Do(p.initFuncMap) |
| 48 | return p.funcMap |
| 49 | } |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 50 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 51 | func (p *Presentation) TemplateFuncs() template.FuncMap { |
| 52 | p.initFuncMapOnce.Do(p.initFuncMap) |
| 53 | return p.templateFuncs |
| 54 | } |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 55 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 56 | func (p *Presentation) initFuncMap() { |
| 57 | if p.Corpus == nil { |
| 58 | panic("nil Presentation.Corpus") |
| 59 | } |
| 60 | p.templateFuncs = template.FuncMap{ |
| 61 | "code": p.code, |
| 62 | } |
| 63 | p.funcMap = template.FuncMap{ |
| 64 | // various helpers |
| 65 | "filename": filenameFunc, |
| 66 | "repeat": strings.Repeat, |
Devon H. O'Dell | 57f659e | 2017-12-22 17:30:39 -0800 | [diff] [blame] | 67 | "since": p.Corpus.pkgAPIInfo.sinceVersionFunc, |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 68 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 69 | // access to FileInfos (directory listings) |
| 70 | "fileInfoName": fileInfoNameFunc, |
| 71 | "fileInfoTime": fileInfoTimeFunc, |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 72 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 73 | // access to search result information |
| 74 | "infoKind_html": infoKind_htmlFunc, |
| 75 | "infoLine": p.infoLineFunc, |
| 76 | "infoSnippet_html": p.infoSnippet_htmlFunc, |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 77 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 78 | // formatting of AST nodes |
| 79 | "node": p.nodeFunc, |
| 80 | "node_html": p.node_htmlFunc, |
| 81 | "comment_html": comment_htmlFunc, |
Robert Griesemer | 42a4cd3 | 2014-04-07 12:54:28 -0700 | [diff] [blame] | 82 | "sanitize": sanitizeFunc, |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 83 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 84 | // support for URL attributes |
Sina Siadat | 5128de7 | 2016-09-16 17:12:50 +0430 | [diff] [blame] | 85 | "pkgLink": pkgLinkFunc, |
| 86 | "srcLink": srcLinkFunc, |
| 87 | "posLink_url": newPosLink_urlFunc(srcPosLinkFunc), |
| 88 | "docLink": docLinkFunc, |
| 89 | "queryLink": queryLinkFunc, |
| 90 | "srcBreadcrumb": srcBreadcrumbFunc, |
| 91 | "srcToPkgLink": srcToPkgLinkFunc, |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 92 | |
| 93 | // formatting of Examples |
| 94 | "example_html": p.example_htmlFunc, |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 95 | "example_name": p.example_nameFunc, |
| 96 | "example_suffix": p.example_suffixFunc, |
| 97 | |
Alan Donovan | 80c4f06 | 2014-03-14 18:58:22 -0400 | [diff] [blame] | 98 | // formatting of analysis information |
| 99 | "callgraph_html": p.callgraph_htmlFunc, |
| 100 | "implements_html": p.implements_htmlFunc, |
| 101 | "methodset_html": p.methodset_htmlFunc, |
| 102 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 103 | // formatting of Notes |
| 104 | "noteTitle": noteTitle, |
Matthew Holt | f671283 | 2015-04-17 17:09:18 -0600 | [diff] [blame] | 105 | |
| 106 | // Number operation |
| 107 | "multiply": multiply, |
Ted Kornish | 396c176 | 2017-01-18 10:52:36 -0800 | [diff] [blame] | 108 | |
| 109 | // formatting of PageInfoMode query string |
| 110 | "modeQueryString": modeQueryString, |
Agniva De Sarker | 16d1af8 | 2018-01-28 19:53:55 +0530 | [diff] [blame] | 111 | |
| 112 | // check whether to display third party section or not |
| 113 | "hasThirdParty": hasThirdParty, |
Agniva De Sarker | 3e7aa9e | 2018-08-11 13:33:17 +0530 | [diff] [blame] | 114 | |
| 115 | // get the no. of columns to split the toc in search page |
| 116 | "tocColCount": tocColCount, |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 117 | } |
Brad Garcia | d9d5e51 | 2013-11-01 13:20:32 -0400 | [diff] [blame] | 118 | if p.URLForSrc != nil { |
| 119 | p.funcMap["srcLink"] = p.URLForSrc |
| 120 | } |
| 121 | if p.URLForSrcPos != nil { |
| 122 | p.funcMap["posLink_url"] = newPosLink_urlFunc(p.URLForSrcPos) |
| 123 | } |
Brad Garcia | ff7cfaf | 2013-12-04 10:37:01 -0500 | [diff] [blame] | 124 | if p.URLForSrcQuery != nil { |
| 125 | p.funcMap["queryLink"] = p.URLForSrcQuery |
| 126 | } |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 127 | } |
| 128 | |
Matthew Holt | f671283 | 2015-04-17 17:09:18 -0600 | [diff] [blame] | 129 | func multiply(a, b int) int { return a * b } |
| 130 | |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 131 | func filenameFunc(path string) string { |
| 132 | _, localname := pathpkg.Split(path) |
| 133 | return localname |
| 134 | } |
| 135 | |
| 136 | func fileInfoNameFunc(fi os.FileInfo) string { |
| 137 | name := fi.Name() |
| 138 | if fi.IsDir() { |
| 139 | name += "/" |
| 140 | } |
| 141 | return name |
| 142 | } |
| 143 | |
| 144 | func fileInfoTimeFunc(fi os.FileInfo) string { |
| 145 | if t := fi.ModTime(); t.Unix() != 0 { |
| 146 | return t.Local().String() |
| 147 | } |
| 148 | return "" // don't return epoch if time is obviously not set |
| 149 | } |
| 150 | |
| 151 | // The strings in infoKinds must be properly html-escaped. |
| 152 | var infoKinds = [nKinds]string{ |
| 153 | PackageClause: "package clause", |
| 154 | ImportDecl: "import decl", |
| 155 | ConstDecl: "const decl", |
| 156 | TypeDecl: "type decl", |
| 157 | VarDecl: "var decl", |
| 158 | FuncDecl: "func decl", |
| 159 | MethodDecl: "method decl", |
| 160 | Use: "use", |
| 161 | } |
| 162 | |
| 163 | func infoKind_htmlFunc(info SpotInfo) string { |
| 164 | return infoKinds[info.Kind()] // infoKind entries are html-escaped |
| 165 | } |
| 166 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 167 | func (p *Presentation) infoLineFunc(info SpotInfo) int { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 168 | line := info.Lori() |
| 169 | if info.IsIndex() { |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 170 | index, _ := p.Corpus.searchIndex.Get() |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 171 | if index != nil { |
| 172 | line = index.(*Index).Snippet(line).Line |
| 173 | } else { |
| 174 | // no line information available because |
| 175 | // we don't have an index - this should |
| 176 | // never happen; be conservative and don't |
| 177 | // crash |
| 178 | line = 0 |
| 179 | } |
| 180 | } |
| 181 | return line |
| 182 | } |
| 183 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 184 | func (p *Presentation) infoSnippet_htmlFunc(info SpotInfo) string { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 185 | if info.IsIndex() { |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 186 | index, _ := p.Corpus.searchIndex.Get() |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 187 | // Snippet.Text was HTML-escaped when it was generated |
| 188 | return index.(*Index).Snippet(info.Lori()).Text |
| 189 | } |
| 190 | return `<span class="alert">no snippet text available</span>` |
| 191 | } |
| 192 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 193 | func (p *Presentation) nodeFunc(info *PageInfo, node interface{}) string { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 194 | var buf bytes.Buffer |
Brad Fitzpatrick | 32950ab | 2018-07-17 21:23:18 +0000 | [diff] [blame] | 195 | p.writeNode(&buf, info, info.FSet, node) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 196 | return buf.String() |
| 197 | } |
| 198 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 199 | func (p *Presentation) node_htmlFunc(info *PageInfo, node interface{}, linkify bool) string { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 200 | var buf1 bytes.Buffer |
Brad Fitzpatrick | 32950ab | 2018-07-17 21:23:18 +0000 | [diff] [blame] | 201 | p.writeNode(&buf1, info, info.FSet, node) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 202 | |
| 203 | var buf2 bytes.Buffer |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 204 | if n, _ := node.(ast.Node); n != nil && linkify && p.DeclLinks { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 205 | LinkifyText(&buf2, buf1.Bytes(), n) |
Brad Fitzpatrick | 0f65b31 | 2016-11-29 23:53:36 +0000 | [diff] [blame] | 206 | if st, name := isStructTypeDecl(n); st != nil { |
| 207 | addStructFieldIDAttributes(&buf2, name, st) |
| 208 | } |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 209 | } else { |
| 210 | FormatText(&buf2, buf1.Bytes(), -1, true, "", nil) |
| 211 | } |
| 212 | |
| 213 | return buf2.String() |
| 214 | } |
| 215 | |
Brad Fitzpatrick | 0f65b31 | 2016-11-29 23:53:36 +0000 | [diff] [blame] | 216 | // isStructTypeDecl checks whether n is a struct declaration. |
| 217 | // It either returns a non-nil StructType and its name, or zero values. |
| 218 | func isStructTypeDecl(n ast.Node) (st *ast.StructType, name string) { |
| 219 | gd, ok := n.(*ast.GenDecl) |
| 220 | if !ok || gd.Tok != token.TYPE { |
| 221 | return nil, "" |
| 222 | } |
| 223 | if gd.Lparen > 0 { |
| 224 | // Parenthesized type. Who does that, anyway? |
| 225 | // TODO: Reportedly gri does. Fix this to handle that too. |
| 226 | return nil, "" |
| 227 | } |
| 228 | if len(gd.Specs) != 1 { |
| 229 | return nil, "" |
| 230 | } |
| 231 | ts, ok := gd.Specs[0].(*ast.TypeSpec) |
| 232 | if !ok { |
| 233 | return nil, "" |
| 234 | } |
| 235 | st, ok = ts.Type.(*ast.StructType) |
| 236 | if !ok { |
| 237 | return nil, "" |
| 238 | } |
| 239 | return st, ts.Name.Name |
| 240 | } |
| 241 | |
| 242 | // addStructFieldIDAttributes modifies the contents of buf such that |
| 243 | // all struct fields of the named struct have <span id='name.Field'> |
| 244 | // in them, so people can link to /#Struct.Field. |
| 245 | func addStructFieldIDAttributes(buf *bytes.Buffer, name string, st *ast.StructType) { |
| 246 | if st.Fields == nil { |
| 247 | return |
| 248 | } |
Brad Fitzpatrick | 61efd71 | 2017-01-20 18:35:00 +0000 | [diff] [blame] | 249 | // needsLink is a set of identifiers that still need to be |
| 250 | // linked, where value == key, to avoid an allocation in func |
| 251 | // linkedField. |
| 252 | needsLink := make(map[string]string) |
| 253 | |
Brad Fitzpatrick | 0f65b31 | 2016-11-29 23:53:36 +0000 | [diff] [blame] | 254 | for _, f := range st.Fields.List { |
| 255 | if len(f.Names) == 0 { |
| 256 | continue |
| 257 | } |
| 258 | fieldName := f.Names[0].Name |
Brad Fitzpatrick | 61efd71 | 2017-01-20 18:35:00 +0000 | [diff] [blame] | 259 | needsLink[fieldName] = fieldName |
Brad Fitzpatrick | e5f9a3d | 2016-11-30 23:49:04 +0000 | [diff] [blame] | 260 | } |
Brad Fitzpatrick | 61efd71 | 2017-01-20 18:35:00 +0000 | [diff] [blame] | 261 | var newBuf bytes.Buffer |
| 262 | foreachLine(buf.Bytes(), func(line []byte) { |
| 263 | if fieldName := linkedField(line, needsLink); fieldName != "" { |
| 264 | fmt.Fprintf(&newBuf, `<span id="%s.%s"></span>`, name, fieldName) |
| 265 | delete(needsLink, fieldName) |
| 266 | } |
| 267 | newBuf.Write(line) |
| 268 | }) |
| 269 | buf.Reset() |
| 270 | buf.Write(newBuf.Bytes()) |
Brad Fitzpatrick | e5f9a3d | 2016-11-30 23:49:04 +0000 | [diff] [blame] | 271 | } |
| 272 | |
| 273 | // foreachLine calls fn for each line of in, where a line includes |
| 274 | // the trailing "\n", except on the last line, if it doesn't exist. |
| 275 | func foreachLine(in []byte, fn func(line []byte)) { |
| 276 | for len(in) > 0 { |
| 277 | nl := bytes.IndexByte(in, '\n') |
| 278 | if nl == -1 { |
| 279 | fn(in) |
| 280 | return |
| 281 | } |
| 282 | fn(in[:nl+1]) |
| 283 | in = in[nl+1:] |
| 284 | } |
| 285 | } |
| 286 | |
| 287 | // commentPrefix is the line prefix for comments after they've been HTMLified. |
| 288 | var commentPrefix = []byte(`<span class="comment">// `) |
| 289 | |
Brad Fitzpatrick | 61efd71 | 2017-01-20 18:35:00 +0000 | [diff] [blame] | 290 | // linkedField determines whether the given line starts with an |
Anthony Fok | dc59f20 | 2019-11-19 13:32:09 -0700 | [diff] [blame] | 291 | // identifier in the provided ids map (mapping from identifier to the |
Brad Fitzpatrick | 61efd71 | 2017-01-20 18:35:00 +0000 | [diff] [blame] | 292 | // same identifier). The line can start with either an identifier or |
| 293 | // an identifier in a comment. If one matches, it returns the |
| 294 | // identifier that matched. Otherwise it returns the empty string. |
| 295 | func linkedField(line []byte, ids map[string]string) string { |
Brad Fitzpatrick | e5f9a3d | 2016-11-30 23:49:04 +0000 | [diff] [blame] | 296 | line = bytes.TrimSpace(line) |
| 297 | |
| 298 | // For fields with a doc string of the |
| 299 | // conventional form, we put the new span into |
| 300 | // the comment instead of the field. |
| 301 | // The "conventional" form is a complete sentence |
| 302 | // per https://golang.org/s/style#comment-sentences like: |
| 303 | // |
| 304 | // // Foo is an optional Fooer to foo the foos. |
| 305 | // Foo Fooer |
| 306 | // |
| 307 | // In this case, we want the #StructName.Foo |
| 308 | // link to make the browser go to the comment |
| 309 | // line "Foo is an optional Fooer" instead of |
| 310 | // the "Foo Fooer" line, which could otherwise |
| 311 | // obscure the docs above the browser's "fold". |
| 312 | // |
| 313 | // TODO: do this better, so it works for all |
| 314 | // comments, including unconventional ones. |
Rebecca Stambler | 207d3de | 2019-11-20 22:43:00 -0500 | [diff] [blame] | 315 | line = bytes.TrimPrefix(line, commentPrefix) |
Brad Fitzpatrick | 61efd71 | 2017-01-20 18:35:00 +0000 | [diff] [blame] | 316 | id := scanIdentifier(line) |
| 317 | if len(id) == 0 { |
| 318 | // No leading identifier. Avoid map lookup for |
| 319 | // somewhat common case. |
| 320 | return "" |
| 321 | } |
| 322 | return ids[string(id)] |
| 323 | } |
| 324 | |
| 325 | // scanIdentifier scans a valid Go identifier off the front of v and |
| 326 | // either returns a subslice of v if there's a valid identifier, or |
| 327 | // returns a zero-length slice. |
| 328 | func scanIdentifier(v []byte) []byte { |
| 329 | var n int // number of leading bytes of v belonging to an identifier |
| 330 | for { |
| 331 | r, width := utf8.DecodeRune(v[n:]) |
| 332 | if !(isLetter(r) || n > 0 && isDigit(r)) { |
| 333 | break |
| 334 | } |
| 335 | n += width |
| 336 | } |
| 337 | return v[:n] |
| 338 | } |
| 339 | |
| 340 | func isLetter(ch rune) bool { |
| 341 | return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= utf8.RuneSelf && unicode.IsLetter(ch) |
| 342 | } |
| 343 | |
| 344 | func isDigit(ch rune) bool { |
| 345 | return '0' <= ch && ch <= '9' || ch >= utf8.RuneSelf && unicode.IsDigit(ch) |
Brad Fitzpatrick | e5f9a3d | 2016-11-30 23:49:04 +0000 | [diff] [blame] | 346 | } |
Brad Fitzpatrick | 0f65b31 | 2016-11-29 23:53:36 +0000 | [diff] [blame] | 347 | |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 348 | func comment_htmlFunc(comment string) string { |
| 349 | var buf bytes.Buffer |
| 350 | // TODO(gri) Provide list of words (e.g. function parameters) |
| 351 | // to be emphasized by ToHTML. |
| 352 | doc.ToHTML(&buf, comment, nil) // does html-escaping |
| 353 | return buf.String() |
| 354 | } |
| 355 | |
Robert Griesemer | 42a4cd3 | 2014-04-07 12:54:28 -0700 | [diff] [blame] | 356 | // sanitizeFunc sanitizes the argument src by replacing newlines with |
| 357 | // blanks, removing extra blanks, and by removing trailing whitespace |
| 358 | // and commas before closing parentheses. |
| 359 | func sanitizeFunc(src string) string { |
| 360 | buf := make([]byte, len(src)) |
| 361 | j := 0 // buf index |
| 362 | comma := -1 // comma index if >= 0 |
| 363 | for i := 0; i < len(src); i++ { |
| 364 | ch := src[i] |
| 365 | switch ch { |
| 366 | case '\t', '\n', ' ': |
| 367 | // ignore whitespace at the beginning, after a blank, or after opening parentheses |
| 368 | if j == 0 { |
| 369 | continue |
| 370 | } |
| 371 | if p := buf[j-1]; p == ' ' || p == '(' || p == '{' || p == '[' { |
| 372 | continue |
| 373 | } |
| 374 | // replace all whitespace with blanks |
| 375 | ch = ' ' |
| 376 | case ',': |
| 377 | comma = j |
| 378 | case ')', '}', ']': |
| 379 | // remove any trailing comma |
| 380 | if comma >= 0 { |
| 381 | j = comma |
| 382 | } |
| 383 | // remove any trailing whitespace |
| 384 | if j > 0 && buf[j-1] == ' ' { |
| 385 | j-- |
| 386 | } |
| 387 | default: |
| 388 | comma = -1 |
| 389 | } |
| 390 | buf[j] = ch |
| 391 | j++ |
| 392 | } |
| 393 | // remove trailing blank, if any |
| 394 | if j > 0 && buf[j-1] == ' ' { |
| 395 | j-- |
| 396 | } |
| 397 | return string(buf[:j]) |
| 398 | } |
| 399 | |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 400 | type PageInfo struct { |
Andrew Bonventre | 4e70a1b | 2017-08-02 16:37:59 -0400 | [diff] [blame] | 401 | Dirname string // directory containing the package |
| 402 | Err error // error or nil |
| 403 | GoogleCN bool // page is being served from golang.google.cn |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 404 | |
Ted Kornish | 396c176 | 2017-01-18 10:52:36 -0800 | [diff] [blame] | 405 | Mode PageInfoMode // display metadata from query string |
| 406 | |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 407 | // package info |
Brad Garcia | 2ca2bfc | 2014-01-30 06:28:19 -0500 | [diff] [blame] | 408 | FSet *token.FileSet // nil if no package documentation |
| 409 | PDoc *doc.Package // nil if no package documentation |
| 410 | Examples []*doc.Example // nil if no example code |
| 411 | Notes map[string][]*doc.Note // nil if no package Notes |
| 412 | PAst map[string]*ast.File // nil if no AST with package exports |
| 413 | IsMain bool // true for package main |
| 414 | IsFiltered bool // true if results were filtered |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 415 | |
Alan Donovan | 80c4f06 | 2014-03-14 18:58:22 -0400 | [diff] [blame] | 416 | // analysis info |
| 417 | TypeInfoIndex map[string]int // index of JSON datum for type T (if -analysis=type) |
| 418 | AnalysisData htmltemplate.JS // array of TypeInfoJSON values |
| 419 | CallGraph htmltemplate.JS // array of PCGNodeJSON values (if -analysis=pointer) |
| 420 | CallGraphIndex map[string]int // maps func name to index in CallGraph |
| 421 | |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 422 | // directory info |
| 423 | Dirs *DirList // nil if no directory information |
| 424 | DirTime time.Time // directory time stamp |
| 425 | DirFlat bool // if set, show directory in a flat (non-indented) manner |
| 426 | } |
| 427 | |
| 428 | func (info *PageInfo) IsEmpty() bool { |
| 429 | return info.Err != nil || info.PAst == nil && info.PDoc == nil && info.Dirs == nil |
| 430 | } |
| 431 | |
| 432 | func pkgLinkFunc(path string) string { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 433 | // because of the irregular mapping under goroot |
| 434 | // we need to correct certain relative paths |
Brad Garcia | 936715c | 2014-06-27 10:25:57 -0400 | [diff] [blame] | 435 | path = strings.TrimPrefix(path, "/") |
| 436 | path = strings.TrimPrefix(path, "src/") |
| 437 | path = strings.TrimPrefix(path, "pkg/") |
| 438 | return "pkg/" + path |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 439 | } |
| 440 | |
Andrew Bonventre | a237aba | 2017-08-03 18:01:02 -0400 | [diff] [blame] | 441 | // srcToPkgLinkFunc builds an <a> tag linking to the package |
| 442 | // documentation of relpath. |
Sina Siadat | 5128de7 | 2016-09-16 17:12:50 +0430 | [diff] [blame] | 443 | func srcToPkgLinkFunc(relpath string) string { |
| 444 | relpath = pkgLinkFunc(relpath) |
Andrew Bonventre | a237aba | 2017-08-03 18:01:02 -0400 | [diff] [blame] | 445 | relpath = pathpkg.Dir(relpath) |
| 446 | if relpath == "pkg" { |
Sina Siadat | 5128de7 | 2016-09-16 17:12:50 +0430 | [diff] [blame] | 447 | return `<a href="/pkg">Index</a>` |
| 448 | } |
Sina Siadat | 5128de7 | 2016-09-16 17:12:50 +0430 | [diff] [blame] | 449 | return fmt.Sprintf(`<a href="/%s">%s</a>`, relpath, relpath[len("pkg/"):]) |
| 450 | } |
| 451 | |
| 452 | // srcBreadcrumbFun converts each segment of relpath to a HTML <a>. |
| 453 | // Each segment links to its corresponding src directories. |
| 454 | func srcBreadcrumbFunc(relpath string) string { |
| 455 | segments := strings.Split(relpath, "/") |
| 456 | var buf bytes.Buffer |
| 457 | var selectedSegment string |
| 458 | var selectedIndex int |
| 459 | |
| 460 | if strings.HasSuffix(relpath, "/") { |
| 461 | // relpath is a directory ending with a "/". |
| 462 | // Selected segment is the segment before the last slash. |
| 463 | selectedIndex = len(segments) - 2 |
| 464 | selectedSegment = segments[selectedIndex] + "/" |
| 465 | } else { |
| 466 | selectedIndex = len(segments) - 1 |
| 467 | selectedSegment = segments[selectedIndex] |
| 468 | } |
| 469 | |
| 470 | for i := range segments[:selectedIndex] { |
| 471 | buf.WriteString(fmt.Sprintf(`<a href="/%s">%s</a>/`, |
| 472 | strings.Join(segments[:i+1], "/"), |
| 473 | segments[i], |
| 474 | )) |
| 475 | } |
| 476 | |
| 477 | buf.WriteString(`<span class="text-muted">`) |
| 478 | buf.WriteString(selectedSegment) |
| 479 | buf.WriteString(`</span>`) |
| 480 | return buf.String() |
| 481 | } |
| 482 | |
Brad Garcia | d9d5e51 | 2013-11-01 13:20:32 -0400 | [diff] [blame] | 483 | func newPosLink_urlFunc(srcPosLinkFunc func(s string, line, low, high int) string) func(info *PageInfo, n interface{}) string { |
| 484 | // n must be an ast.Node or a *doc.Note |
| 485 | return func(info *PageInfo, n interface{}) string { |
| 486 | var pos, end token.Pos |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 487 | |
Brad Garcia | d9d5e51 | 2013-11-01 13:20:32 -0400 | [diff] [blame] | 488 | switch n := n.(type) { |
| 489 | case ast.Node: |
| 490 | pos = n.Pos() |
| 491 | end = n.End() |
| 492 | case *doc.Note: |
| 493 | pos = n.Pos |
| 494 | end = n.End |
| 495 | default: |
| 496 | panic(fmt.Sprintf("wrong type for posLink_url template formatter: %T", n)) |
| 497 | } |
| 498 | |
| 499 | var relpath string |
| 500 | var line int |
| 501 | var low, high int // selection offset range |
| 502 | |
| 503 | if pos.IsValid() { |
| 504 | p := info.FSet.Position(pos) |
| 505 | relpath = p.Filename |
| 506 | line = p.Line |
| 507 | low = p.Offset |
| 508 | } |
| 509 | if end.IsValid() { |
| 510 | high = info.FSet.Position(end).Offset |
| 511 | } |
| 512 | |
| 513 | return srcPosLinkFunc(relpath, line, low, high) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 514 | } |
Brad Garcia | d9d5e51 | 2013-11-01 13:20:32 -0400 | [diff] [blame] | 515 | } |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 516 | |
Brad Garcia | d9d5e51 | 2013-11-01 13:20:32 -0400 | [diff] [blame] | 517 | func srcPosLinkFunc(s string, line, low, high int) string { |
Brad Garcia | 61bd98e | 2014-02-21 19:51:51 -0500 | [diff] [blame] | 518 | s = srcLinkFunc(s) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 519 | var buf bytes.Buffer |
Brad Garcia | d9d5e51 | 2013-11-01 13:20:32 -0400 | [diff] [blame] | 520 | template.HTMLEscape(&buf, []byte(s)) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 521 | // selection ranges are of form "s=low:high" |
| 522 | if low < high { |
| 523 | fmt.Fprintf(&buf, "?s=%d:%d", low, high) // no need for URL escaping |
| 524 | // if we have a selection, position the page |
| 525 | // such that the selection is a bit below the top |
| 526 | line -= 10 |
| 527 | if line < 1 { |
| 528 | line = 1 |
| 529 | } |
| 530 | } |
| 531 | // line id's in html-printed source are of the |
| 532 | // form "L%d" where %d stands for the line number |
| 533 | if line > 0 { |
| 534 | fmt.Fprintf(&buf, "#L%d", line) // no need for URL escaping |
| 535 | } |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 536 | return buf.String() |
| 537 | } |
| 538 | |
| 539 | func srcLinkFunc(s string) string { |
Brad Garcia | 61bd98e | 2014-02-21 19:51:51 -0500 | [diff] [blame] | 540 | s = pathpkg.Clean("/" + s) |
Alan Donovan | 6c93dbf | 2014-09-10 09:02:54 -0400 | [diff] [blame] | 541 | if !strings.HasPrefix(s, "/src/") { |
| 542 | s = "/src" + s |
Brad Garcia | 61bd98e | 2014-02-21 19:51:51 -0500 | [diff] [blame] | 543 | } |
| 544 | return s |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 545 | } |
| 546 | |
Brad Garcia | ff7cfaf | 2013-12-04 10:37:01 -0500 | [diff] [blame] | 547 | // queryLinkFunc returns a URL for a line in a source file with a highlighted |
| 548 | // query term. |
| 549 | // s is expected to be a path to a source file. |
| 550 | // query is expected to be a string that has already been appropriately escaped |
| 551 | // for use in a URL query. |
| 552 | func queryLinkFunc(s, query string, line int) string { |
| 553 | url := pathpkg.Clean("/"+s) + "?h=" + query |
| 554 | if line > 0 { |
| 555 | url += "#L" + strconv.Itoa(line) |
| 556 | } |
| 557 | return url |
| 558 | } |
| 559 | |
Brad Garcia | f3faf8b | 2013-11-21 11:55:42 -0500 | [diff] [blame] | 560 | func docLinkFunc(s string, ident string) string { |
Brad Garcia | 936715c | 2014-06-27 10:25:57 -0400 | [diff] [blame] | 561 | return pathpkg.Clean("/pkg/"+s) + "/#" + ident |
Brad Garcia | f3faf8b | 2013-11-21 11:55:42 -0500 | [diff] [blame] | 562 | } |
| 563 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 564 | func (p *Presentation) example_htmlFunc(info *PageInfo, funcName string) string { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 565 | var buf bytes.Buffer |
| 566 | for _, eg := range info.Examples { |
| 567 | name := stripExampleSuffix(eg.Name) |
| 568 | |
| 569 | if name != funcName { |
| 570 | continue |
| 571 | } |
| 572 | |
| 573 | // print code |
| 574 | cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments} |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 575 | code := p.node_htmlFunc(info, cnode, true) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 576 | out := eg.Output |
| 577 | wholeFile := true |
| 578 | |
| 579 | // Additional formatting if this is a function body. |
| 580 | if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' { |
| 581 | wholeFile = false |
| 582 | // remove surrounding braces |
| 583 | code = code[1 : n-1] |
| 584 | // unindent |
Jay Conrod | 00f7cd5 | 2017-02-07 17:34:43 -0500 | [diff] [blame] | 585 | code = replaceLeadingIndentation(code, strings.Repeat(" ", p.TabWidth), "") |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 586 | // remove output comment |
| 587 | if loc := exampleOutputRx.FindStringIndex(code); loc != nil { |
| 588 | code = strings.TrimSpace(code[:loc[0]]) |
| 589 | } |
| 590 | } |
| 591 | |
| 592 | // Write out the playground code in standard Go style |
| 593 | // (use tabs, no comment highlight, etc). |
| 594 | play := "" |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 595 | if eg.Play != nil && p.ShowPlayground { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 596 | var buf bytes.Buffer |
Mostyn Bramley-Moore | 9e9f7f6 | 2018-07-31 17:27:32 +0000 | [diff] [blame] | 597 | eg.Play.Comments = filterOutBuildAnnotations(eg.Play.Comments) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 598 | if err := format.Node(&buf, info.FSet, eg.Play); err != nil { |
| 599 | log.Print(err) |
| 600 | } else { |
| 601 | play = buf.String() |
| 602 | } |
| 603 | } |
| 604 | |
| 605 | // Drop output, as the output comment will appear in the code. |
| 606 | if wholeFile && play == "" { |
| 607 | out = "" |
| 608 | } |
| 609 | |
Brad Fitzpatrick | 66f0d6e | 2013-07-18 13:51:17 +1000 | [diff] [blame] | 610 | if p.ExampleHTML == nil { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 611 | out = "" |
| 612 | return "" |
| 613 | } |
| 614 | |
Brad Fitzpatrick | 66f0d6e | 2013-07-18 13:51:17 +1000 | [diff] [blame] | 615 | err := p.ExampleHTML.Execute(&buf, struct { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 616 | Name, Doc, Code, Play, Output string |
Andrew Bonventre | 4e70a1b | 2017-08-02 16:37:59 -0400 | [diff] [blame] | 617 | GoogleCN bool |
| 618 | }{eg.Name, eg.Doc, code, play, out, info.GoogleCN}) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 619 | if err != nil { |
| 620 | log.Print(err) |
| 621 | } |
| 622 | } |
| 623 | return buf.String() |
| 624 | } |
| 625 | |
Mostyn Bramley-Moore | 9e9f7f6 | 2018-07-31 17:27:32 +0000 | [diff] [blame] | 626 | func filterOutBuildAnnotations(cg []*ast.CommentGroup) []*ast.CommentGroup { |
| 627 | if len(cg) == 0 { |
| 628 | return cg |
| 629 | } |
| 630 | |
| 631 | for i := range cg { |
| 632 | if !strings.HasPrefix(cg[i].Text(), "+build ") { |
| 633 | // Found the first non-build tag, return from here until the end |
| 634 | // of the slice. |
| 635 | return cg[i:] |
| 636 | } |
| 637 | } |
| 638 | |
| 639 | // There weren't any non-build tags, return an empty slice. |
| 640 | return []*ast.CommentGroup{} |
| 641 | } |
| 642 | |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 643 | // example_nameFunc takes an example function name and returns its display |
| 644 | // name. For example, "Foo_Bar_quux" becomes "Foo.Bar (Quux)". |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 645 | func (p *Presentation) example_nameFunc(s string) string { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 646 | name, suffix := splitExampleName(s) |
| 647 | // replace _ with . for method names |
| 648 | name = strings.Replace(name, "_", ".", 1) |
| 649 | // use "Package" if no name provided |
| 650 | if name == "" { |
| 651 | name = "Package" |
| 652 | } |
| 653 | return name + suffix |
| 654 | } |
| 655 | |
| 656 | // example_suffixFunc takes an example function name and returns its suffix in |
| 657 | // parenthesized form. For example, "Foo_Bar_quux" becomes " (Quux)". |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 658 | func (p *Presentation) example_suffixFunc(name string) string { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 659 | _, suffix := splitExampleName(name) |
| 660 | return suffix |
| 661 | } |
| 662 | |
Alan Donovan | 80c4f06 | 2014-03-14 18:58:22 -0400 | [diff] [blame] | 663 | // implements_html returns the "> Implements" toggle for a package-level named type. |
| 664 | // Its contents are populated from JSON data by client-side JS at load time. |
| 665 | func (p *Presentation) implements_htmlFunc(info *PageInfo, typeName string) string { |
| 666 | if p.ImplementsHTML == nil { |
| 667 | return "" |
| 668 | } |
| 669 | index, ok := info.TypeInfoIndex[typeName] |
| 670 | if !ok { |
| 671 | return "" |
| 672 | } |
| 673 | var buf bytes.Buffer |
| 674 | err := p.ImplementsHTML.Execute(&buf, struct{ Index int }{index}) |
| 675 | if err != nil { |
| 676 | log.Print(err) |
| 677 | } |
| 678 | return buf.String() |
| 679 | } |
| 680 | |
| 681 | // methodset_html returns the "> Method set" toggle for a package-level named type. |
| 682 | // Its contents are populated from JSON data by client-side JS at load time. |
| 683 | func (p *Presentation) methodset_htmlFunc(info *PageInfo, typeName string) string { |
| 684 | if p.MethodSetHTML == nil { |
| 685 | return "" |
| 686 | } |
| 687 | index, ok := info.TypeInfoIndex[typeName] |
| 688 | if !ok { |
| 689 | return "" |
| 690 | } |
| 691 | var buf bytes.Buffer |
| 692 | err := p.MethodSetHTML.Execute(&buf, struct{ Index int }{index}) |
| 693 | if err != nil { |
| 694 | log.Print(err) |
| 695 | } |
| 696 | return buf.String() |
| 697 | } |
| 698 | |
| 699 | // callgraph_html returns the "> Call graph" toggle for a package-level func. |
| 700 | // Its contents are populated from JSON data by client-side JS at load time. |
| 701 | func (p *Presentation) callgraph_htmlFunc(info *PageInfo, recv, name string) string { |
| 702 | if p.CallGraphHTML == nil { |
| 703 | return "" |
| 704 | } |
| 705 | if recv != "" { |
| 706 | // Format must match (*ssa.Function).RelString(). |
| 707 | name = fmt.Sprintf("(%s).%s", recv, name) |
| 708 | } |
| 709 | index, ok := info.CallGraphIndex[name] |
| 710 | if !ok { |
| 711 | return "" |
| 712 | } |
| 713 | var buf bytes.Buffer |
| 714 | err := p.CallGraphHTML.Execute(&buf, struct{ Index int }{index}) |
| 715 | if err != nil { |
| 716 | log.Print(err) |
| 717 | } |
| 718 | return buf.String() |
| 719 | } |
| 720 | |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 721 | func noteTitle(note string) string { |
| 722 | return strings.Title(strings.ToLower(note)) |
| 723 | } |
| 724 | |
| 725 | func startsWithUppercase(s string) bool { |
| 726 | r, _ := utf8.DecodeRuneInString(s) |
| 727 | return unicode.IsUpper(r) |
| 728 | } |
| 729 | |
Andrew Gerrand | 477d3b9 | 2016-04-14 09:31:55 +1000 | [diff] [blame] | 730 | var exampleOutputRx = regexp.MustCompile(`(?i)//[[:space:]]*(unordered )?output:`) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 731 | |
| 732 | // stripExampleSuffix strips lowercase braz in Foo_braz or Foo_Bar_braz from name |
| 733 | // while keeping uppercase Braz in Foo_Braz. |
| 734 | func stripExampleSuffix(name string) string { |
| 735 | if i := strings.LastIndex(name, "_"); i != -1 { |
| 736 | if i < len(name)-1 && !startsWithUppercase(name[i+1:]) { |
| 737 | name = name[:i] |
| 738 | } |
| 739 | } |
| 740 | return name |
| 741 | } |
| 742 | |
| 743 | func splitExampleName(s string) (name, suffix string) { |
| 744 | i := strings.LastIndex(s, "_") |
| 745 | if 0 <= i && i < len(s)-1 && !startsWithUppercase(s[i+1:]) { |
| 746 | name = s[:i] |
| 747 | suffix = " (" + strings.Title(s[i+1:]) + ")" |
| 748 | return |
| 749 | } |
| 750 | name = s |
| 751 | return |
| 752 | } |
| 753 | |
Jay Conrod | 00f7cd5 | 2017-02-07 17:34:43 -0500 | [diff] [blame] | 754 | // replaceLeadingIndentation replaces oldIndent at the beginning of each line |
| 755 | // with newIndent. This is used for formatting examples. Raw strings that |
| 756 | // span multiple lines are handled specially: oldIndent is not removed (since |
| 757 | // go/printer will not add any indentation there), but newIndent is added |
| 758 | // (since we may still want leading indentation). |
| 759 | func replaceLeadingIndentation(body, oldIndent, newIndent string) string { |
| 760 | // Handle indent at the beginning of the first line. After this, we handle |
| 761 | // indentation only after a newline. |
| 762 | var buf bytes.Buffer |
| 763 | if strings.HasPrefix(body, oldIndent) { |
| 764 | buf.WriteString(newIndent) |
| 765 | body = body[len(oldIndent):] |
| 766 | } |
| 767 | |
| 768 | // Use a state machine to keep track of whether we're in a string or |
| 769 | // rune literal while we process the rest of the code. |
| 770 | const ( |
| 771 | codeState = iota |
| 772 | runeState |
| 773 | interpretedStringState |
| 774 | rawStringState |
| 775 | ) |
| 776 | searchChars := []string{ |
| 777 | "'\"`\n", // codeState |
| 778 | `\'`, // runeState |
| 779 | `\"`, // interpretedStringState |
| 780 | "`\n", // rawStringState |
| 781 | // newlineState does not need to search |
| 782 | } |
| 783 | state := codeState |
| 784 | for { |
| 785 | i := strings.IndexAny(body, searchChars[state]) |
| 786 | if i < 0 { |
| 787 | buf.WriteString(body) |
| 788 | break |
| 789 | } |
| 790 | c := body[i] |
| 791 | buf.WriteString(body[:i+1]) |
| 792 | body = body[i+1:] |
| 793 | switch state { |
| 794 | case codeState: |
| 795 | switch c { |
| 796 | case '\'': |
| 797 | state = runeState |
| 798 | case '"': |
| 799 | state = interpretedStringState |
| 800 | case '`': |
| 801 | state = rawStringState |
| 802 | case '\n': |
| 803 | if strings.HasPrefix(body, oldIndent) { |
| 804 | buf.WriteString(newIndent) |
| 805 | body = body[len(oldIndent):] |
| 806 | } |
| 807 | } |
| 808 | |
| 809 | case runeState: |
| 810 | switch c { |
| 811 | case '\\': |
| 812 | r, size := utf8.DecodeRuneInString(body) |
| 813 | buf.WriteRune(r) |
| 814 | body = body[size:] |
| 815 | case '\'': |
| 816 | state = codeState |
| 817 | } |
| 818 | |
| 819 | case interpretedStringState: |
| 820 | switch c { |
| 821 | case '\\': |
| 822 | r, size := utf8.DecodeRuneInString(body) |
| 823 | buf.WriteRune(r) |
| 824 | body = body[size:] |
| 825 | case '"': |
| 826 | state = codeState |
| 827 | } |
| 828 | |
| 829 | case rawStringState: |
| 830 | switch c { |
| 831 | case '`': |
| 832 | state = codeState |
| 833 | case '\n': |
| 834 | buf.WriteString(newIndent) |
| 835 | } |
| 836 | } |
| 837 | } |
| 838 | return buf.String() |
| 839 | } |
| 840 | |
Brad Fitzpatrick | 32950ab | 2018-07-17 21:23:18 +0000 | [diff] [blame] | 841 | // writeNode writes the AST node x to w. |
| 842 | // |
| 843 | // The provided fset must be non-nil. The pageInfo is optional. If |
| 844 | // present, the pageInfo is used to add comments to struct fields to |
| 845 | // say which version of Go introduced them. |
| 846 | func (p *Presentation) writeNode(w io.Writer, pageInfo *PageInfo, fset *token.FileSet, x interface{}) { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 847 | // convert trailing tabs into spaces using a tconv filter |
| 848 | // to ensure a good outcome in most browsers (there may still |
| 849 | // be tabs in comments and strings, but converting those into |
| 850 | // the right number of spaces is much harder) |
| 851 | // |
| 852 | // TODO(gri) rethink printer flags - perhaps tconv can be eliminated |
| 853 | // with an another printer mode (which is more efficiently |
| 854 | // implemented in the printer than here with another layer) |
Brad Fitzpatrick | 32950ab | 2018-07-17 21:23:18 +0000 | [diff] [blame] | 855 | |
| 856 | var pkgName, structName string |
| 857 | var apiInfo pkgAPIVersions |
| 858 | if gd, ok := x.(*ast.GenDecl); ok && pageInfo != nil && pageInfo.PDoc != nil && |
| 859 | p.Corpus != nil && |
| 860 | gd.Tok == token.TYPE && len(gd.Specs) != 0 { |
| 861 | pkgName = pageInfo.PDoc.ImportPath |
| 862 | if ts, ok := gd.Specs[0].(*ast.TypeSpec); ok { |
| 863 | if _, ok := ts.Type.(*ast.StructType); ok { |
| 864 | structName = ts.Name.Name |
| 865 | } |
| 866 | } |
| 867 | apiInfo = p.Corpus.pkgAPIInfo[pkgName] |
| 868 | } |
| 869 | |
| 870 | var out = w |
| 871 | var buf bytes.Buffer |
| 872 | if structName != "" { |
| 873 | out = &buf |
| 874 | } |
| 875 | |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 876 | mode := printer.TabIndent | printer.UseSpaces |
Brad Fitzpatrick | 32950ab | 2018-07-17 21:23:18 +0000 | [diff] [blame] | 877 | err := (&printer.Config{Mode: mode, Tabwidth: p.TabWidth}).Fprint(&tconv{p: p, output: out}, fset, x) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 878 | if err != nil { |
| 879 | log.Print(err) |
| 880 | } |
Brad Fitzpatrick | 32950ab | 2018-07-17 21:23:18 +0000 | [diff] [blame] | 881 | |
Ainar Garipov | feee8ac | 2019-09-11 09:14:36 +0300 | [diff] [blame] | 882 | // Add comments to struct fields saying which Go version introduced them. |
Brad Fitzpatrick | 32950ab | 2018-07-17 21:23:18 +0000 | [diff] [blame] | 883 | if structName != "" { |
| 884 | fieldSince := apiInfo.fieldSince[structName] |
| 885 | typeSince := apiInfo.typeSince[structName] |
| 886 | // Add/rewrite comments on struct fields to note which Go version added them. |
| 887 | var buf2 bytes.Buffer |
| 888 | buf2.Grow(buf.Len() + len(" // Added in Go 1.n")*10) |
| 889 | bs := bufio.NewScanner(&buf) |
| 890 | for bs.Scan() { |
| 891 | line := bs.Bytes() |
| 892 | field := firstIdent(line) |
| 893 | var since string |
| 894 | if field != "" { |
| 895 | since = fieldSince[field] |
| 896 | if since != "" && since == typeSince { |
| 897 | // Don't highlight field versions if they were the |
| 898 | // same as the struct itself. |
| 899 | since = "" |
| 900 | } |
| 901 | } |
| 902 | if since == "" { |
| 903 | buf2.Write(line) |
| 904 | } else { |
| 905 | if bytes.Contains(line, slashSlash) { |
| 906 | line = bytes.TrimRight(line, " \t.") |
| 907 | buf2.Write(line) |
| 908 | buf2.WriteString("; added in Go ") |
| 909 | } else { |
| 910 | buf2.Write(line) |
| 911 | buf2.WriteString(" // Go ") |
| 912 | } |
| 913 | buf2.WriteString(since) |
| 914 | } |
| 915 | buf2.WriteByte('\n') |
| 916 | } |
| 917 | w.Write(buf2.Bytes()) |
| 918 | } |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 919 | } |
| 920 | |
Brad Fitzpatrick | 32950ab | 2018-07-17 21:23:18 +0000 | [diff] [blame] | 921 | var slashSlash = []byte("//") |
| 922 | |
Robert Griesemer | 42a4cd3 | 2014-04-07 12:54:28 -0700 | [diff] [blame] | 923 | // WriteNode writes x to w. |
| 924 | // TODO(bgarcia) Is this method needed? It's just a wrapper for p.writeNode. |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 925 | func (p *Presentation) WriteNode(w io.Writer, fset *token.FileSet, x interface{}) { |
Brad Fitzpatrick | 32950ab | 2018-07-17 21:23:18 +0000 | [diff] [blame] | 926 | p.writeNode(w, nil, fset, x) |
| 927 | } |
| 928 | |
| 929 | // firstIdent returns the first identifier in x. |
| 930 | // This actually parses "identifiers" that begin with numbers too, but we |
| 931 | // never feed it such input, so it's fine. |
| 932 | func firstIdent(x []byte) string { |
| 933 | x = bytes.TrimSpace(x) |
| 934 | i := bytes.IndexFunc(x, func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsNumber(r) }) |
| 935 | if i == -1 { |
| 936 | return string(x) |
| 937 | } |
| 938 | return string(x[:i]) |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 939 | } |