github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/tools/present/code.go (about) 1 // Copyright 2012 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 present 6 7 import ( 8 "bufio" 9 "bytes" 10 "fmt" 11 "html/template" 12 "path/filepath" 13 "regexp" 14 "strconv" 15 "strings" 16 ) 17 18 // Is the playground available? 19 var PlayEnabled = false 20 21 // TOOD(adg): replace the PlayEnabled flag with something less spaghetti-like. 22 // Instead this will probably be determined by a template execution Context 23 // value that contains various global metadata required when rendering 24 // templates. 25 26 func init() { 27 Register("code", parseCode) 28 Register("play", parseCode) 29 } 30 31 type Code struct { 32 Text template.HTML 33 Play bool // runnable code 34 FileName string // file name 35 Ext string // file extension 36 Raw []byte // content of the file 37 } 38 39 func (c Code) TemplateName() string { return "code" } 40 41 // The input line is a .code or .play entry with a file name and an optional HLfoo marker on the end. 42 // Anything between the file and HL (if any) is an address expression, which we treat as a string here. 43 // We pick off the HL first, for easy parsing. 44 var ( 45 highlightRE = regexp.MustCompile(`\s+HL([a-zA-Z0-9_]+)?$`) 46 hlCommentRE = regexp.MustCompile(`(.+) // HL(.*)$`) 47 codeRE = regexp.MustCompile(`\.(code|play)\s+((?:(?:-edit|-numbers)\s+)*)([^\s]+)(?:\s+(.*))?$`) 48 ) 49 50 // parseCode parses a code present directive. Its syntax: 51 // .code [-numbers] [-edit] <filename> [address] [highlight] 52 // The directive may also be ".play" if the snippet is executable. 53 func parseCode(ctx *Context, sourceFile string, sourceLine int, cmd string) (Elem, error) { 54 cmd = strings.TrimSpace(cmd) 55 56 // Pull off the HL, if any, from the end of the input line. 57 highlight := "" 58 if hl := highlightRE.FindStringSubmatchIndex(cmd); len(hl) == 4 { 59 highlight = cmd[hl[2]:hl[3]] 60 cmd = cmd[:hl[2]-2] 61 } 62 63 // Parse the remaining command line. 64 // Arguments: 65 // args[0]: whole match 66 // args[1]: .code/.play 67 // args[2]: flags ("-edit -numbers") 68 // args[3]: file name 69 // args[4]: optional address 70 args := codeRE.FindStringSubmatch(cmd) 71 if len(args) != 5 { 72 return nil, fmt.Errorf("%s:%d: syntax error for .code/.play invocation", sourceFile, sourceLine) 73 } 74 command, flags, file, addr := args[1], args[2], args[3], strings.TrimSpace(args[4]) 75 play := command == "play" && PlayEnabled 76 77 // Read in code file and (optionally) match address. 78 filename := filepath.Join(filepath.Dir(sourceFile), file) 79 textBytes, err := ctx.ReadFile(filename) 80 if err != nil { 81 return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err) 82 } 83 lo, hi, err := addrToByteRange(addr, 0, textBytes) 84 if err != nil { 85 return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err) 86 } 87 88 // Acme pattern matches can stop mid-line, 89 // so run to end of line in both directions if not at line start/end. 90 for lo > 0 && textBytes[lo-1] != '\n' { 91 lo-- 92 } 93 if hi > 0 { 94 for hi < len(textBytes) && textBytes[hi-1] != '\n' { 95 hi++ 96 } 97 } 98 99 lines := codeLines(textBytes, lo, hi) 100 101 data := &codeTemplateData{ 102 Lines: formatLines(lines, highlight), 103 Edit: strings.Contains(flags, "-edit"), 104 Numbers: strings.Contains(flags, "-numbers"), 105 } 106 107 // Include before and after in a hidden span for playground code. 108 if play { 109 data.Prefix = textBytes[:lo] 110 data.Suffix = textBytes[hi:] 111 } 112 113 var buf bytes.Buffer 114 if err := codeTemplate.Execute(&buf, data); err != nil { 115 return nil, err 116 } 117 return Code{ 118 Text: template.HTML(buf.String()), 119 Play: play, 120 FileName: filepath.Base(filename), 121 Ext: filepath.Ext(filename), 122 Raw: rawCode(lines), 123 }, nil 124 } 125 126 // formatLines returns a new slice of codeLine with the given lines 127 // replacing tabs with spaces and adding highlighting where needed. 128 func formatLines(lines []codeLine, highlight string) []codeLine { 129 formatted := make([]codeLine, len(lines)) 130 for i, line := range lines { 131 // Replace tabs with spaces, which work better in HTML. 132 line.L = strings.Replace(line.L, "\t", " ", -1) 133 134 // Highlight lines that end with "// HL[highlight]" 135 // and strip the magic comment. 136 if m := hlCommentRE.FindStringSubmatch(line.L); m != nil { 137 line.L = m[1] 138 line.HL = m[2] == highlight 139 } 140 141 formatted[i] = line 142 } 143 return formatted 144 } 145 146 // rawCode returns the code represented by the given codeLines without any kind 147 // of formatting. 148 func rawCode(lines []codeLine) []byte { 149 b := new(bytes.Buffer) 150 for _, line := range lines { 151 b.WriteString(line.L) 152 b.WriteByte('\n') 153 } 154 return b.Bytes() 155 } 156 157 type codeTemplateData struct { 158 Lines []codeLine 159 Prefix, Suffix []byte 160 Edit, Numbers bool 161 } 162 163 var leadingSpaceRE = regexp.MustCompile(`^[ \t]*`) 164 165 var codeTemplate = template.Must(template.New("code").Funcs(template.FuncMap{ 166 "trimSpace": strings.TrimSpace, 167 "leadingSpace": leadingSpaceRE.FindString, 168 }).Parse(codeTemplateHTML)) 169 170 const codeTemplateHTML = ` 171 {{with .Prefix}}<pre style="display: none"><span>{{printf "%s" .}}</span></pre>{{end}} 172 173 <pre{{if .Edit}} contenteditable="true" spellcheck="false"{{end}}{{if .Numbers}} class="numbers"{{end}}>{{/* 174 */}}{{range .Lines}}<span num="{{.N}}">{{/* 175 */}}{{if .HL}}{{leadingSpace .L}}<b>{{trimSpace .L}}</b>{{/* 176 */}}{{else}}{{.L}}{{end}}{{/* 177 */}}</span> 178 {{end}}</pre> 179 180 {{with .Suffix}}<pre style="display: none"><span>{{printf "%s" .}}</span></pre>{{end}} 181 ` 182 183 // codeLine represents a line of code extracted from a source file. 184 type codeLine struct { 185 L string // The line of code. 186 N int // The line number from the source file. 187 HL bool // Whether the line should be highlighted. 188 } 189 190 // codeLines takes a source file and returns the lines that 191 // span the byte range specified by start and end. 192 // It discards lines that end in "OMIT". 193 func codeLines(src []byte, start, end int) (lines []codeLine) { 194 startLine := 1 195 for i, b := range src { 196 if i == start { 197 break 198 } 199 if b == '\n' { 200 startLine++ 201 } 202 } 203 s := bufio.NewScanner(bytes.NewReader(src[start:end])) 204 for n := startLine; s.Scan(); n++ { 205 l := s.Text() 206 if strings.HasSuffix(l, "OMIT") { 207 continue 208 } 209 lines = append(lines, codeLine{L: l, N: n}) 210 } 211 // Trim leading and trailing blank lines. 212 for len(lines) > 0 && len(lines[0].L) == 0 { 213 lines = lines[1:] 214 } 215 for len(lines) > 0 && len(lines[len(lines)-1].L) == 0 { 216 lines = lines[:len(lines)-1] 217 } 218 return 219 } 220 221 func parseArgs(name string, line int, args []string) (res []interface{}, err error) { 222 res = make([]interface{}, len(args)) 223 for i, v := range args { 224 if len(v) == 0 { 225 return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v) 226 } 227 switch v[0] { 228 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 229 n, err := strconv.Atoi(v) 230 if err != nil { 231 return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v) 232 } 233 res[i] = n 234 case '/': 235 if len(v) < 2 || v[len(v)-1] != '/' { 236 return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v) 237 } 238 res[i] = v 239 case '$': 240 res[i] = "$" 241 case '_': 242 if len(v) == 1 { 243 // Do nothing; "_" indicates an intentionally empty parameter. 244 break 245 } 246 fallthrough 247 default: 248 return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v) 249 } 250 } 251 return 252 } 253 254 // parseArg returns the integer or string value of the argument and tells which it is. 255 func parseArg(arg interface{}, max int) (ival int, sval string, isInt bool, err error) { 256 switch n := arg.(type) { 257 case int: 258 if n <= 0 || n > max { 259 return 0, "", false, fmt.Errorf("%d is out of range", n) 260 } 261 return n, "", true, nil 262 case string: 263 return 0, n, false, nil 264 } 265 return 0, "", false, fmt.Errorf("unrecognized argument %v type %T", arg, arg) 266 } 267 268 // match identifies the input line that matches the pattern in a code invocation. 269 // If start>0, match lines starting there rather than at the beginning. 270 // The return value is 1-indexed. 271 func match(file string, start int, lines []string, pattern string) (int, error) { 272 // $ matches the end of the file. 273 if pattern == "$" { 274 if len(lines) == 0 { 275 return 0, fmt.Errorf("%q: empty file", file) 276 } 277 return len(lines), nil 278 } 279 // /regexp/ matches the line that matches the regexp. 280 if len(pattern) > 2 && pattern[0] == '/' && pattern[len(pattern)-1] == '/' { 281 re, err := regexp.Compile(pattern[1 : len(pattern)-1]) 282 if err != nil { 283 return 0, err 284 } 285 for i := start; i < len(lines); i++ { 286 if re.MatchString(lines[i]) { 287 return i + 1, nil 288 } 289 } 290 return 0, fmt.Errorf("%s: no match for %#q", file, pattern) 291 } 292 return 0, fmt.Errorf("unrecognized pattern: %q", pattern) 293 }