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. |
| 10 | package godoc |
| 11 | |
| 12 | import ( |
| 13 | "bytes" |
| 14 | "fmt" |
| 15 | "go/ast" |
| 16 | "go/doc" |
| 17 | "go/format" |
| 18 | "go/printer" |
| 19 | "go/token" |
| 20 | "io" |
| 21 | "log" |
| 22 | "os" |
| 23 | pathpkg "path" |
| 24 | "regexp" |
Brad Garcia | ff7cfaf | 2013-12-04 10:37:01 -0500 | [diff] [blame] | 25 | "strconv" |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 26 | "strings" |
| 27 | "text/template" |
| 28 | "time" |
| 29 | "unicode" |
| 30 | "unicode/utf8" |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 31 | ) |
| 32 | |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 33 | // Fake relative package path for built-ins. Documentation for all globals |
| 34 | // (not just exported ones) will be shown for packages in this directory. |
Brad Fitzpatrick | 705bb7f | 2013-07-19 10:27:53 +1000 | [diff] [blame] | 35 | const builtinPkgPath = "builtin" |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 36 | |
| 37 | // FuncMap defines template functions used in godoc templates. |
| 38 | // |
| 39 | // Convention: template function names ending in "_html" or "_url" produce |
| 40 | // HTML- or URL-escaped strings; all other function results may |
| 41 | // require explicit escaping in the template. |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 42 | func (p *Presentation) FuncMap() template.FuncMap { |
| 43 | p.initFuncMapOnce.Do(p.initFuncMap) |
| 44 | return p.funcMap |
| 45 | } |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 46 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 47 | func (p *Presentation) TemplateFuncs() template.FuncMap { |
| 48 | p.initFuncMapOnce.Do(p.initFuncMap) |
| 49 | return p.templateFuncs |
| 50 | } |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 51 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 52 | func (p *Presentation) initFuncMap() { |
| 53 | if p.Corpus == nil { |
| 54 | panic("nil Presentation.Corpus") |
| 55 | } |
| 56 | p.templateFuncs = template.FuncMap{ |
| 57 | "code": p.code, |
| 58 | } |
| 59 | p.funcMap = template.FuncMap{ |
| 60 | // various helpers |
| 61 | "filename": filenameFunc, |
| 62 | "repeat": strings.Repeat, |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 63 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 64 | // access to FileInfos (directory listings) |
| 65 | "fileInfoName": fileInfoNameFunc, |
| 66 | "fileInfoTime": fileInfoTimeFunc, |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 67 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 68 | // access to search result information |
| 69 | "infoKind_html": infoKind_htmlFunc, |
| 70 | "infoLine": p.infoLineFunc, |
| 71 | "infoSnippet_html": p.infoSnippet_htmlFunc, |
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 | // formatting of AST nodes |
| 74 | "node": p.nodeFunc, |
| 75 | "node_html": p.node_htmlFunc, |
| 76 | "comment_html": comment_htmlFunc, |
| 77 | "comment_text": comment_textFunc, |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 78 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 79 | // support for URL attributes |
| 80 | "pkgLink": pkgLinkFunc, |
| 81 | "srcLink": srcLinkFunc, |
Brad Garcia | d9d5e51 | 2013-11-01 13:20:32 -0400 | [diff] [blame] | 82 | "posLink_url": newPosLink_urlFunc(srcPosLinkFunc), |
Brad Garcia | f3faf8b | 2013-11-21 11:55:42 -0500 | [diff] [blame] | 83 | "docLink": docLinkFunc, |
Brad Garcia | ff7cfaf | 2013-12-04 10:37:01 -0500 | [diff] [blame] | 84 | "queryLink": queryLinkFunc, |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 85 | |
| 86 | // formatting of Examples |
| 87 | "example_html": p.example_htmlFunc, |
| 88 | "example_text": p.example_textFunc, |
| 89 | "example_name": p.example_nameFunc, |
| 90 | "example_suffix": p.example_suffixFunc, |
| 91 | |
| 92 | // formatting of Notes |
| 93 | "noteTitle": noteTitle, |
| 94 | } |
Brad Garcia | d9d5e51 | 2013-11-01 13:20:32 -0400 | [diff] [blame] | 95 | if p.URLForSrc != nil { |
| 96 | p.funcMap["srcLink"] = p.URLForSrc |
| 97 | } |
| 98 | if p.URLForSrcPos != nil { |
| 99 | p.funcMap["posLink_url"] = newPosLink_urlFunc(p.URLForSrcPos) |
| 100 | } |
Brad Garcia | ff7cfaf | 2013-12-04 10:37:01 -0500 | [diff] [blame] | 101 | if p.URLForSrcQuery != nil { |
| 102 | p.funcMap["queryLink"] = p.URLForSrcQuery |
| 103 | } |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 104 | } |
| 105 | |
| 106 | func filenameFunc(path string) string { |
| 107 | _, localname := pathpkg.Split(path) |
| 108 | return localname |
| 109 | } |
| 110 | |
| 111 | func fileInfoNameFunc(fi os.FileInfo) string { |
| 112 | name := fi.Name() |
| 113 | if fi.IsDir() { |
| 114 | name += "/" |
| 115 | } |
| 116 | return name |
| 117 | } |
| 118 | |
| 119 | func fileInfoTimeFunc(fi os.FileInfo) string { |
| 120 | if t := fi.ModTime(); t.Unix() != 0 { |
| 121 | return t.Local().String() |
| 122 | } |
| 123 | return "" // don't return epoch if time is obviously not set |
| 124 | } |
| 125 | |
| 126 | // The strings in infoKinds must be properly html-escaped. |
| 127 | var infoKinds = [nKinds]string{ |
| 128 | PackageClause: "package clause", |
| 129 | ImportDecl: "import decl", |
| 130 | ConstDecl: "const decl", |
| 131 | TypeDecl: "type decl", |
| 132 | VarDecl: "var decl", |
| 133 | FuncDecl: "func decl", |
| 134 | MethodDecl: "method decl", |
| 135 | Use: "use", |
| 136 | } |
| 137 | |
| 138 | func infoKind_htmlFunc(info SpotInfo) string { |
| 139 | return infoKinds[info.Kind()] // infoKind entries are html-escaped |
| 140 | } |
| 141 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 142 | func (p *Presentation) infoLineFunc(info SpotInfo) int { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 143 | line := info.Lori() |
| 144 | if info.IsIndex() { |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 145 | index, _ := p.Corpus.searchIndex.Get() |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 146 | if index != nil { |
| 147 | line = index.(*Index).Snippet(line).Line |
| 148 | } else { |
| 149 | // no line information available because |
| 150 | // we don't have an index - this should |
| 151 | // never happen; be conservative and don't |
| 152 | // crash |
| 153 | line = 0 |
| 154 | } |
| 155 | } |
| 156 | return line |
| 157 | } |
| 158 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 159 | func (p *Presentation) infoSnippet_htmlFunc(info SpotInfo) string { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 160 | if info.IsIndex() { |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 161 | index, _ := p.Corpus.searchIndex.Get() |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 162 | // Snippet.Text was HTML-escaped when it was generated |
| 163 | return index.(*Index).Snippet(info.Lori()).Text |
| 164 | } |
| 165 | return `<span class="alert">no snippet text available</span>` |
| 166 | } |
| 167 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 168 | func (p *Presentation) nodeFunc(info *PageInfo, node interface{}) string { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 169 | var buf bytes.Buffer |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 170 | p.writeNode(&buf, info.FSet, node) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 171 | return buf.String() |
| 172 | } |
| 173 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 174 | func (p *Presentation) node_htmlFunc(info *PageInfo, node interface{}, linkify bool) string { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 175 | var buf1 bytes.Buffer |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 176 | p.writeNode(&buf1, info.FSet, node) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 177 | |
| 178 | var buf2 bytes.Buffer |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 179 | if n, _ := node.(ast.Node); n != nil && linkify && p.DeclLinks { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 180 | LinkifyText(&buf2, buf1.Bytes(), n) |
| 181 | } else { |
| 182 | FormatText(&buf2, buf1.Bytes(), -1, true, "", nil) |
| 183 | } |
| 184 | |
| 185 | return buf2.String() |
| 186 | } |
| 187 | |
| 188 | func comment_htmlFunc(comment string) string { |
| 189 | var buf bytes.Buffer |
| 190 | // TODO(gri) Provide list of words (e.g. function parameters) |
| 191 | // to be emphasized by ToHTML. |
| 192 | doc.ToHTML(&buf, comment, nil) // does html-escaping |
| 193 | return buf.String() |
| 194 | } |
| 195 | |
| 196 | // punchCardWidth is the number of columns of fixed-width |
| 197 | // characters to assume when wrapping text. Very few people |
| 198 | // use terminals or cards smaller than 80 characters, so 80 it is. |
| 199 | // We do not try to sniff the environment or the tty to adapt to |
| 200 | // the situation; instead, by using a constant we make sure that |
| 201 | // godoc always produces the same output regardless of context, |
| 202 | // a consistency that is lost otherwise. For example, if we sniffed |
| 203 | // the environment or tty, then http://golang.org/pkg/math/?m=text |
| 204 | // would depend on the width of the terminal where godoc started, |
| 205 | // which is clearly bogus. More generally, the Unix tools that behave |
| 206 | // differently when writing to a tty than when writing to a file have |
| 207 | // a history of causing confusion (compare `ls` and `ls | cat`), and we |
| 208 | // want to avoid that mistake here. |
| 209 | const punchCardWidth = 80 |
| 210 | |
| 211 | func comment_textFunc(comment, indent, preIndent string) string { |
| 212 | var buf bytes.Buffer |
| 213 | doc.ToText(&buf, comment, indent, preIndent, punchCardWidth-2*len(indent)) |
| 214 | return buf.String() |
| 215 | } |
| 216 | |
| 217 | type PageInfo struct { |
| 218 | Dirname string // directory containing the package |
| 219 | Err error // error or nil |
| 220 | |
| 221 | // package info |
Brad Garcia | 2ca2bfc | 2014-01-30 06:28:19 -0500 | [diff] [blame^] | 222 | FSet *token.FileSet // nil if no package documentation |
| 223 | PDoc *doc.Package // nil if no package documentation |
| 224 | Examples []*doc.Example // nil if no example code |
| 225 | Notes map[string][]*doc.Note // nil if no package Notes |
| 226 | PAst map[string]*ast.File // nil if no AST with package exports |
| 227 | IsMain bool // true for package main |
| 228 | IsFiltered bool // true if results were filtered |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 229 | |
| 230 | // directory info |
| 231 | Dirs *DirList // nil if no directory information |
| 232 | DirTime time.Time // directory time stamp |
| 233 | DirFlat bool // if set, show directory in a flat (non-indented) manner |
| 234 | } |
| 235 | |
| 236 | func (info *PageInfo) IsEmpty() bool { |
| 237 | return info.Err != nil || info.PAst == nil && info.PDoc == nil && info.Dirs == nil |
| 238 | } |
| 239 | |
| 240 | func pkgLinkFunc(path string) string { |
| 241 | relpath := path[1:] |
| 242 | // because of the irregular mapping under goroot |
| 243 | // we need to correct certain relative paths |
| 244 | relpath = strings.TrimPrefix(relpath, "src/pkg/") |
Brad Fitzpatrick | 705bb7f | 2013-07-19 10:27:53 +1000 | [diff] [blame] | 245 | return "pkg/" + relpath // remove trailing '/' for relative URL |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 246 | } |
| 247 | |
Brad Garcia | d9d5e51 | 2013-11-01 13:20:32 -0400 | [diff] [blame] | 248 | func newPosLink_urlFunc(srcPosLinkFunc func(s string, line, low, high int) string) func(info *PageInfo, n interface{}) string { |
| 249 | // n must be an ast.Node or a *doc.Note |
| 250 | return func(info *PageInfo, n interface{}) string { |
| 251 | var pos, end token.Pos |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 252 | |
Brad Garcia | d9d5e51 | 2013-11-01 13:20:32 -0400 | [diff] [blame] | 253 | switch n := n.(type) { |
| 254 | case ast.Node: |
| 255 | pos = n.Pos() |
| 256 | end = n.End() |
| 257 | case *doc.Note: |
| 258 | pos = n.Pos |
| 259 | end = n.End |
| 260 | default: |
| 261 | panic(fmt.Sprintf("wrong type for posLink_url template formatter: %T", n)) |
| 262 | } |
| 263 | |
| 264 | var relpath string |
| 265 | var line int |
| 266 | var low, high int // selection offset range |
| 267 | |
| 268 | if pos.IsValid() { |
| 269 | p := info.FSet.Position(pos) |
| 270 | relpath = p.Filename |
| 271 | line = p.Line |
| 272 | low = p.Offset |
| 273 | } |
| 274 | if end.IsValid() { |
| 275 | high = info.FSet.Position(end).Offset |
| 276 | } |
| 277 | |
| 278 | return srcPosLinkFunc(relpath, line, low, high) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 279 | } |
Brad Garcia | d9d5e51 | 2013-11-01 13:20:32 -0400 | [diff] [blame] | 280 | } |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 281 | |
Brad Garcia | d9d5e51 | 2013-11-01 13:20:32 -0400 | [diff] [blame] | 282 | func srcPosLinkFunc(s string, line, low, high int) string { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 283 | var buf bytes.Buffer |
Brad Garcia | d9d5e51 | 2013-11-01 13:20:32 -0400 | [diff] [blame] | 284 | template.HTMLEscape(&buf, []byte(s)) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 285 | // selection ranges are of form "s=low:high" |
| 286 | if low < high { |
| 287 | fmt.Fprintf(&buf, "?s=%d:%d", low, high) // no need for URL escaping |
| 288 | // if we have a selection, position the page |
| 289 | // such that the selection is a bit below the top |
| 290 | line -= 10 |
| 291 | if line < 1 { |
| 292 | line = 1 |
| 293 | } |
| 294 | } |
| 295 | // line id's in html-printed source are of the |
| 296 | // form "L%d" where %d stands for the line number |
| 297 | if line > 0 { |
| 298 | fmt.Fprintf(&buf, "#L%d", line) // no need for URL escaping |
| 299 | } |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 300 | return buf.String() |
| 301 | } |
| 302 | |
| 303 | func srcLinkFunc(s string) string { |
| 304 | return pathpkg.Clean("/" + s) |
| 305 | } |
| 306 | |
Brad Garcia | ff7cfaf | 2013-12-04 10:37:01 -0500 | [diff] [blame] | 307 | // queryLinkFunc returns a URL for a line in a source file with a highlighted |
| 308 | // query term. |
| 309 | // s is expected to be a path to a source file. |
| 310 | // query is expected to be a string that has already been appropriately escaped |
| 311 | // for use in a URL query. |
| 312 | func queryLinkFunc(s, query string, line int) string { |
| 313 | url := pathpkg.Clean("/"+s) + "?h=" + query |
| 314 | if line > 0 { |
| 315 | url += "#L" + strconv.Itoa(line) |
| 316 | } |
| 317 | return url |
| 318 | } |
| 319 | |
Brad Garcia | f3faf8b | 2013-11-21 11:55:42 -0500 | [diff] [blame] | 320 | func docLinkFunc(s string, ident string) string { |
| 321 | s = strings.TrimPrefix(s, "/src") |
| 322 | return pathpkg.Clean("/"+s) + "/#" + ident |
| 323 | } |
| 324 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 325 | func (p *Presentation) example_textFunc(info *PageInfo, funcName, indent string) string { |
| 326 | if !p.ShowExamples { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 327 | return "" |
| 328 | } |
| 329 | |
| 330 | var buf bytes.Buffer |
| 331 | first := true |
| 332 | for _, eg := range info.Examples { |
| 333 | name := stripExampleSuffix(eg.Name) |
| 334 | if name != funcName { |
| 335 | continue |
| 336 | } |
| 337 | |
| 338 | if !first { |
| 339 | buf.WriteString("\n") |
| 340 | } |
| 341 | first = false |
| 342 | |
| 343 | // print code |
| 344 | cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments} |
| 345 | var buf1 bytes.Buffer |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 346 | p.writeNode(&buf1, info.FSet, cnode) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 347 | code := buf1.String() |
| 348 | // Additional formatting if this is a function body. |
| 349 | if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' { |
| 350 | // remove surrounding braces |
| 351 | code = code[1 : n-1] |
| 352 | // unindent |
| 353 | code = strings.Replace(code, "\n ", "\n", -1) |
| 354 | } |
| 355 | code = strings.Trim(code, "\n") |
| 356 | code = strings.Replace(code, "\n", "\n\t", -1) |
| 357 | |
| 358 | buf.WriteString(indent) |
| 359 | buf.WriteString("Example:\n\t") |
| 360 | buf.WriteString(code) |
| 361 | buf.WriteString("\n") |
| 362 | } |
| 363 | return buf.String() |
| 364 | } |
| 365 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 366 | func (p *Presentation) example_htmlFunc(info *PageInfo, funcName string) string { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 367 | var buf bytes.Buffer |
| 368 | for _, eg := range info.Examples { |
| 369 | name := stripExampleSuffix(eg.Name) |
| 370 | |
| 371 | if name != funcName { |
| 372 | continue |
| 373 | } |
| 374 | |
| 375 | // print code |
| 376 | cnode := &printer.CommentedNode{Node: eg.Code, Comments: eg.Comments} |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 377 | code := p.node_htmlFunc(info, cnode, true) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 378 | out := eg.Output |
| 379 | wholeFile := true |
| 380 | |
| 381 | // Additional formatting if this is a function body. |
| 382 | if n := len(code); n >= 2 && code[0] == '{' && code[n-1] == '}' { |
| 383 | wholeFile = false |
| 384 | // remove surrounding braces |
| 385 | code = code[1 : n-1] |
| 386 | // unindent |
| 387 | code = strings.Replace(code, "\n ", "\n", -1) |
| 388 | // remove output comment |
| 389 | if loc := exampleOutputRx.FindStringIndex(code); loc != nil { |
| 390 | code = strings.TrimSpace(code[:loc[0]]) |
| 391 | } |
| 392 | } |
| 393 | |
| 394 | // Write out the playground code in standard Go style |
| 395 | // (use tabs, no comment highlight, etc). |
| 396 | play := "" |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 397 | if eg.Play != nil && p.ShowPlayground { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 398 | var buf bytes.Buffer |
| 399 | if err := format.Node(&buf, info.FSet, eg.Play); err != nil { |
| 400 | log.Print(err) |
| 401 | } else { |
| 402 | play = buf.String() |
| 403 | } |
| 404 | } |
| 405 | |
| 406 | // Drop output, as the output comment will appear in the code. |
| 407 | if wholeFile && play == "" { |
| 408 | out = "" |
| 409 | } |
| 410 | |
Brad Fitzpatrick | 66f0d6e | 2013-07-18 13:51:17 +1000 | [diff] [blame] | 411 | if p.ExampleHTML == nil { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 412 | out = "" |
| 413 | return "" |
| 414 | } |
| 415 | |
Brad Fitzpatrick | 66f0d6e | 2013-07-18 13:51:17 +1000 | [diff] [blame] | 416 | err := p.ExampleHTML.Execute(&buf, struct { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 417 | Name, Doc, Code, Play, Output string |
| 418 | }{eg.Name, eg.Doc, code, play, out}) |
| 419 | if err != nil { |
| 420 | log.Print(err) |
| 421 | } |
| 422 | } |
| 423 | return buf.String() |
| 424 | } |
| 425 | |
| 426 | // example_nameFunc takes an example function name and returns its display |
| 427 | // name. For example, "Foo_Bar_quux" becomes "Foo.Bar (Quux)". |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 428 | func (p *Presentation) example_nameFunc(s string) string { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 429 | name, suffix := splitExampleName(s) |
| 430 | // replace _ with . for method names |
| 431 | name = strings.Replace(name, "_", ".", 1) |
| 432 | // use "Package" if no name provided |
| 433 | if name == "" { |
| 434 | name = "Package" |
| 435 | } |
| 436 | return name + suffix |
| 437 | } |
| 438 | |
| 439 | // example_suffixFunc takes an example function name and returns its suffix in |
| 440 | // parenthesized form. For example, "Foo_Bar_quux" becomes " (Quux)". |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 441 | func (p *Presentation) example_suffixFunc(name string) string { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 442 | _, suffix := splitExampleName(name) |
| 443 | return suffix |
| 444 | } |
| 445 | |
| 446 | func noteTitle(note string) string { |
| 447 | return strings.Title(strings.ToLower(note)) |
| 448 | } |
| 449 | |
| 450 | func startsWithUppercase(s string) bool { |
| 451 | r, _ := utf8.DecodeRuneInString(s) |
| 452 | return unicode.IsUpper(r) |
| 453 | } |
| 454 | |
| 455 | var exampleOutputRx = regexp.MustCompile(`(?i)//[[:space:]]*output:`) |
| 456 | |
| 457 | // stripExampleSuffix strips lowercase braz in Foo_braz or Foo_Bar_braz from name |
| 458 | // while keeping uppercase Braz in Foo_Braz. |
| 459 | func stripExampleSuffix(name string) string { |
| 460 | if i := strings.LastIndex(name, "_"); i != -1 { |
| 461 | if i < len(name)-1 && !startsWithUppercase(name[i+1:]) { |
| 462 | name = name[:i] |
| 463 | } |
| 464 | } |
| 465 | return name |
| 466 | } |
| 467 | |
| 468 | func splitExampleName(s string) (name, suffix string) { |
| 469 | i := strings.LastIndex(s, "_") |
| 470 | if 0 <= i && i < len(s)-1 && !startsWithUppercase(s[i+1:]) { |
| 471 | name = s[:i] |
| 472 | suffix = " (" + strings.Title(s[i+1:]) + ")" |
| 473 | return |
| 474 | } |
| 475 | name = s |
| 476 | return |
| 477 | } |
| 478 | |
| 479 | // Write an AST node to w. |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 480 | func (p *Presentation) writeNode(w io.Writer, fset *token.FileSet, x interface{}) { |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 481 | // convert trailing tabs into spaces using a tconv filter |
| 482 | // to ensure a good outcome in most browsers (there may still |
| 483 | // be tabs in comments and strings, but converting those into |
| 484 | // the right number of spaces is much harder) |
| 485 | // |
| 486 | // TODO(gri) rethink printer flags - perhaps tconv can be eliminated |
| 487 | // with an another printer mode (which is more efficiently |
| 488 | // implemented in the printer than here with another layer) |
| 489 | mode := printer.TabIndent | printer.UseSpaces |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 490 | err := (&printer.Config{Mode: mode, Tabwidth: p.TabWidth}).Fprint(&tconv{p: p, output: w}, fset, x) |
Brad Fitzpatrick | ca3319f | 2013-07-17 17:17:12 +1000 | [diff] [blame] | 491 | if err != nil { |
| 492 | log.Print(err) |
| 493 | } |
| 494 | } |
| 495 | |
Brad Fitzpatrick | 5395cfe | 2013-07-18 13:14:09 +1000 | [diff] [blame] | 496 | // WriteNote writes x to w. |
| 497 | func (p *Presentation) WriteNode(w io.Writer, fset *token.FileSet, x interface{}) { |
| 498 | p.writeNode(w, fset, x) |
| 499 | } |