1// Copyright 2011 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// Template support for writing HTML documents. 6// Documents that include Template: true in their 7// metadata are executed as input to text/template. 8// 9// This file defines functions for those templates to invoke. 10 11// The template uses the function "code" to inject program 12// source into the output by extracting code from files and 13// injecting them as HTML-escaped <pre> blocks. 14// 15// The syntax is simple: 1, 2, or 3 space-separated arguments: 16// 17// Whole file: 18// {{code "foo.go"}} 19// One line (here the signature of main): 20// {{code "foo.go" `/^func.main/`}} 21// Block of text, determined by start and end (here the body of main): 22// {{code "foo.go" `/^func.main/` `/^}/` 23// 24// Patterns can be `/regular expression/`, a decimal number, or "$" 25// to signify the end of the file. In multi-line matches, 26// lines that end with the four characters 27// OMIT 28// are omitted from the output, making it easy to provide marker 29// lines in the input that will not appear in the output but are easy 30// to identify by pattern. 31 32package godoc 33 34import ( 35 "bytes" 36 "fmt" 37 "log" 38 "regexp" 39 "strings" 40 41 "golang.org/x/tools/godoc/vfs" 42) 43 44// Functions in this file panic on error, but the panic is recovered 45// to an error by 'code'. 46 47// contents reads and returns the content of the named file 48// (from the virtual file system, so for example /doc refers to $GOROOT/doc). 49func (c *Corpus) contents(name string) string { 50 file, err := vfs.ReadFile(c.fs, name) 51 if err != nil { 52 log.Panic(err) 53 } 54 return string(file) 55} 56 57// stringFor returns a textual representation of the arg, formatted according to its nature. 58func stringFor(arg interface{}) string { 59 switch arg := arg.(type) { 60 case int: 61 return fmt.Sprintf("%d", arg) 62 case string: 63 if len(arg) > 2 && arg[0] == '/' && arg[len(arg)-1] == '/' { 64 return fmt.Sprintf("%#q", arg) 65 } 66 return fmt.Sprintf("%q", arg) 67 default: 68 log.Panicf("unrecognized argument: %v type %T", arg, arg) 69 } 70 return "" 71} 72 73func (p *Presentation) code(file string, arg ...interface{}) (s string, err error) { 74 defer func() { 75 if r := recover(); r != nil { 76 err = fmt.Errorf("%v", r) 77 } 78 }() 79 80 text := p.Corpus.contents(file) 81 var command string 82 switch len(arg) { 83 case 0: 84 // text is already whole file. 85 command = fmt.Sprintf("code %q", file) 86 case 1: 87 command = fmt.Sprintf("code %q %s", file, stringFor(arg[0])) 88 text = p.Corpus.oneLine(file, text, arg[0]) 89 case 2: 90 command = fmt.Sprintf("code %q %s %s", file, stringFor(arg[0]), stringFor(arg[1])) 91 text = p.Corpus.multipleLines(file, text, arg[0], arg[1]) 92 default: 93 return "", fmt.Errorf("incorrect code invocation: code %q [%v, ...] (%d arguments)", file, arg[0], len(arg)) 94 } 95 // Trim spaces from output. 96 text = strings.Trim(text, "\n") 97 // Replace tabs by spaces, which work better in HTML. 98 text = strings.Replace(text, "\t", " ", -1) 99 var buf bytes.Buffer 100 // HTML-escape text and syntax-color comments like elsewhere. 101 FormatText(&buf, []byte(text), -1, true, "", nil) 102 // Include the command as a comment. 103 text = fmt.Sprintf("<pre><!--{{%s}}\n-->%s</pre>", command, buf.Bytes()) 104 return text, nil 105} 106 107// parseArg returns the integer or string value of the argument and tells which it is. 108func parseArg(arg interface{}, file string, max int) (ival int, sval string, isInt bool) { 109 switch n := arg.(type) { 110 case int: 111 if n <= 0 || n > max { 112 log.Panicf("%q:%d is out of range", file, n) 113 } 114 return n, "", true 115 case string: 116 return 0, n, false 117 } 118 log.Panicf("unrecognized argument %v type %T", arg, arg) 119 return 120} 121 122// oneLine returns the single line generated by a two-argument code invocation. 123func (c *Corpus) oneLine(file, text string, arg interface{}) string { 124 lines := strings.SplitAfter(c.contents(file), "\n") 125 line, pattern, isInt := parseArg(arg, file, len(lines)) 126 if isInt { 127 return lines[line-1] 128 } 129 return lines[match(file, 0, lines, pattern)-1] 130} 131 132// multipleLines returns the text generated by a three-argument code invocation. 133func (c *Corpus) multipleLines(file, text string, arg1, arg2 interface{}) string { 134 lines := strings.SplitAfter(c.contents(file), "\n") 135 line1, pattern1, isInt1 := parseArg(arg1, file, len(lines)) 136 line2, pattern2, isInt2 := parseArg(arg2, file, len(lines)) 137 if !isInt1 { 138 line1 = match(file, 0, lines, pattern1) 139 } 140 if !isInt2 { 141 line2 = match(file, line1, lines, pattern2) 142 } else if line2 < line1 { 143 log.Panicf("lines out of order for %q: %d %d", text, line1, line2) 144 } 145 for k := line1 - 1; k < line2; k++ { 146 if strings.HasSuffix(lines[k], "OMIT\n") { 147 lines[k] = "" 148 } 149 } 150 return strings.Join(lines[line1-1:line2], "") 151} 152 153// match identifies the input line that matches the pattern in a code invocation. 154// If start>0, match lines starting there rather than at the beginning. 155// The return value is 1-indexed. 156func match(file string, start int, lines []string, pattern string) int { 157 // $ matches the end of the file. 158 if pattern == "$" { 159 if len(lines) == 0 { 160 log.Panicf("%q: empty file", file) 161 } 162 return len(lines) 163 } 164 // /regexp/ matches the line that matches the regexp. 165 if len(pattern) > 2 && pattern[0] == '/' && pattern[len(pattern)-1] == '/' { 166 re, err := regexp.Compile(pattern[1 : len(pattern)-1]) 167 if err != nil { 168 log.Panic(err) 169 } 170 for i := start; i < len(lines); i++ { 171 if re.MatchString(lines[i]) { 172 return i + 1 173 } 174 } 175 log.Panicf("%s: no match for %#q", file, pattern) 176 } 177 log.Panicf("unrecognized pattern: %q", pattern) 178 return 0 179} 180