LEFT | RIGHT |
| 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 |
1 package present | 5 package present |
2 | 6 |
3 import ( | 7 import ( |
| 8 "bufio" |
| 9 "bytes" |
4 "fmt" | 10 "fmt" |
5 "html/template" | 11 "html/template" |
6 "path/filepath" | 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 } |
| 35 |
| 36 func (c Code) TemplateName() string { return "code" } |
| 37 |
| 38 // The input line is a .code or .play entry with a file name and an optional HLf
oo marker on the end. |
| 39 // Anything between the file and HL (if any) is an address expression, which we
treat as a string here. |
| 40 // We pick off the HL first, for easy parsing. |
| 41 var ( |
| 42 highlightRE = regexp.MustCompile(`\s+HL([a-zA-Z0-9_]+)?$`) |
| 43 hlCommentRE = regexp.MustCompile(`(.+) // HL(.*)$`) |
| 44 codeRE = regexp.MustCompile(`\.(code|play)\s+([^\s]+)(\s+)?(.*)?$`) |
| 45 ) |
| 46 |
| 47 func parseCode(ctx *Context, sourceFile string, sourceLine int, cmd string) (Ele
m, error) { |
| 48 cmd = strings.TrimSpace(cmd) |
| 49 |
| 50 // Pull off the HL, if any, from the end of the input line. |
| 51 highlight := "" |
| 52 if hl := highlightRE.FindStringSubmatchIndex(cmd); len(hl) == 4 { |
| 53 highlight = cmd[hl[2]:hl[3]] |
| 54 cmd = cmd[:hl[2]-2] |
| 55 } |
| 56 |
| 57 // Parse the remaining command line. |
| 58 // Arguments: |
| 59 // args[0]: whole match |
| 60 // args[1]: .code/.play |
| 61 // args[2]: file name |
| 62 // args[3]: space, if any, before optional address |
| 63 // args[4]: optional address |
| 64 args := codeRE.FindStringSubmatch(cmd) |
| 65 if len(args) != 5 { |
| 66 return nil, fmt.Errorf("%s:%d: syntax error for .code/.play invo
cation", sourceFile, sourceLine) |
| 67 } |
| 68 command, file, addr := args[1], args[2], strings.TrimSpace(args[4]) |
| 69 play := command == "play" && PlayEnabled |
| 70 |
| 71 // Read in code file and (optionally) match address. |
| 72 filename := filepath.Join(filepath.Dir(sourceFile), file) |
| 73 textBytes, err := ctx.ReadFile(filename) |
| 74 if err != nil { |
| 75 return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err) |
| 76 } |
| 77 lo, hi, err := addrToByteRange(addr, 0, textBytes) |
| 78 if err != nil { |
| 79 return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err) |
| 80 } |
| 81 |
| 82 // Acme pattern matches can stop mid-line, |
| 83 // so run to end of line in both directions if not at line start/end. |
| 84 for lo > 0 && textBytes[lo-1] != '\n' { |
| 85 lo-- |
| 86 } |
| 87 if hi > 0 { |
| 88 for hi < len(textBytes) && textBytes[hi-1] != '\n' { |
| 89 hi++ |
| 90 } |
| 91 } |
| 92 |
| 93 lines := codeLines(textBytes, lo, hi) |
| 94 |
| 95 for i, line := range lines { |
| 96 // Replace tabs by spaces, which work better in HTML. |
| 97 line.L = strings.Replace(line.L, "\t", " ", -1) |
| 98 |
| 99 // Highlight lines that end with "// HL[highlight]" |
| 100 // and strip the magic comment. |
| 101 if m := hlCommentRE.FindStringSubmatch(line.L); m != nil { |
| 102 line.L = m[1] |
| 103 line.HL = m[2] == highlight |
| 104 } |
| 105 |
| 106 lines[i] = line |
| 107 } |
| 108 |
| 109 data := &codeTemplateData{Lines: lines} |
| 110 |
| 111 // Include before and after in a hidden span for playground code. |
| 112 if play { |
| 113 data.Prefix = textBytes[:lo] |
| 114 data.Suffix = textBytes[hi:] |
| 115 } |
| 116 |
| 117 var buf bytes.Buffer |
| 118 if err := codeTemplate.Execute(&buf, data); err != nil { |
| 119 return nil, err |
| 120 } |
| 121 return Code{Text: template.HTML(buf.String()), Play: play}, nil |
| 122 } |
| 123 |
| 124 type codeTemplateData struct { |
| 125 Lines []codeLine |
| 126 Prefix, Suffix []byte |
| 127 } |
| 128 |
| 129 var leadingSpaceRE = regexp.MustCompile(`^[ \t]*`) |
| 130 |
| 131 var codeTemplate = template.Must(template.New("code").Funcs(template.FuncMap{ |
| 132 "trimSpace": strings.TrimSpace, |
| 133 "leadingSpace": leadingSpaceRE.FindString, |
| 134 }).Parse(codeTemplateHTML)) |
| 135 |
| 136 const codeTemplateHTML = ` |
| 137 {{with .Prefix}}<pre style="display: none"><span>{{printf "%s" .}}</span></pre>{
{end}} |
| 138 |
| 139 <pre>{{range .Lines}}<span num="{{.N}}">{{/* |
| 140 */}}{{if .HL}}{{leadingSpace .L}}<b>{{trimSpace .L}}</b>{{/* |
| 141 */}}{{else}}{{.L}}{{end}}{{/* |
| 142 */}}</span> |
| 143 {{end}}</pre> |
| 144 |
| 145 {{with .Suffix}}<pre style="display: none"><span>{{printf "%s" .}}</span></pre>{
{end}} |
| 146 ` |
| 147 |
| 148 // codeLine represents a line of code extracted from a source file. |
| 149 type codeLine struct { |
| 150 L string // The line of code. |
| 151 N int // The line number from the source file. |
| 152 HL bool // Whether the line should be highlighted. |
| 153 } |
| 154 |
| 155 // codeLines takes a source file and returns the lines that |
| 156 // span the byte range specified by start and end. |
| 157 // It discards lines that end in "OMIT". |
| 158 func codeLines(src []byte, start, end int) (lines []codeLine) { |
| 159 startLine := 1 |
| 160 for i, b := range src { |
| 161 if i == start { |
| 162 break |
| 163 } |
| 164 if b == '\n' { |
| 165 startLine++ |
| 166 } |
| 167 } |
| 168 s := bufio.NewScanner(bytes.NewReader(src[start:end])) |
| 169 for n := startLine; s.Scan(); n++ { |
| 170 l := s.Text() |
| 171 if strings.HasSuffix(l, "OMIT") { |
| 172 continue |
| 173 } |
| 174 lines = append(lines, codeLine{L: l, N: n}) |
| 175 } |
| 176 return |
| 177 } |
| 178 |
| 179 func parseArgs(name string, line int, args []string) (res []interface{}, err err
or) { |
| 180 res = make([]interface{}, len(args)) |
| 181 for i, v := range args { |
| 182 if len(v) == 0 { |
| 183 return nil, fmt.Errorf("%s:%d bad code argument %q", nam
e, line, v) |
| 184 } |
| 185 switch v[0] { |
| 186 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': |
| 187 n, err := strconv.Atoi(v) |
| 188 if err != nil { |
| 189 return nil, fmt.Errorf("%s:%d bad code argument
%q", name, line, v) |
| 190 } |
| 191 res[i] = n |
| 192 case '/': |
| 193 if len(v) < 2 || v[len(v)-1] != '/' { |
| 194 return nil, fmt.Errorf("%s:%d bad code argument
%q", name, line, v) |
| 195 } |
| 196 res[i] = v |
| 197 case '$': |
| 198 res[i] = "$" |
| 199 default: |
| 200 return nil, fmt.Errorf("%s:%d bad code argument %q", nam
e, line, v) |
| 201 } |
| 202 } |
| 203 return |
| 204 } |
| 205 |
| 206 // parseArg returns the integer or string value of the argument and tells which
it is. |
| 207 func parseArg(arg interface{}, max int) (ival int, sval string, isInt bool, err
error) { |
| 208 switch n := arg.(type) { |
| 209 case int: |
| 210 if n <= 0 || n > max { |
| 211 return 0, "", false, fmt.Errorf("%d is out of range", n) |
| 212 } |
| 213 return n, "", true, nil |
| 214 case string: |
| 215 return 0, n, false, nil |
| 216 } |
| 217 return 0, "", false, fmt.Errorf("unrecognized argument %v type %T", arg,
arg) |
| 218 } |
| 219 |
| 220 // oneLine returns the single line generated by a two-argument code invocation. |
| 221 func oneLine(ctx *Context, file, text string, arg interface{}) (line, before, af
ter string, err error) { |
| 222 contentBytes, err := ctx.ReadFile(file) |
| 223 if err != nil { |
| 224 return "", "", "", err |
| 225 } |
| 226 lines := strings.SplitAfter(string(contentBytes), "\n") |
| 227 lineNum, pattern, isInt, err := parseArg(arg, len(lines)) |
| 228 if err != nil { |
| 229 return "", "", "", err |
| 230 } |
| 231 var n int |
| 232 if isInt { |
| 233 n = lineNum - 1 |
| 234 } else { |
| 235 n, err = match(file, 0, lines, pattern) |
| 236 n -= 1 |
| 237 } |
| 238 if err != nil { |
| 239 return "", "", "", err |
| 240 } |
| 241 return lines[n], |
| 242 strings.Join(lines[:n], ""), |
| 243 strings.Join(lines[n+1:], ""), |
| 244 nil |
| 245 } |
| 246 |
| 247 // multipleLines returns the text generated by a three-argument code invocation. |
| 248 func multipleLines(ctx *Context, file string, arg1, arg2 interface{}) (line, bef
ore, after string, err error) { |
| 249 contentBytes, err := ctx.ReadFile(file) |
| 250 lines := strings.SplitAfter(string(contentBytes), "\n") |
| 251 if err != nil { |
| 252 return "", "", "", err |
| 253 } |
| 254 line1, pattern1, isInt1, err := parseArg(arg1, len(lines)) |
| 255 if err != nil { |
| 256 return "", "", "", err |
| 257 } |
| 258 line2, pattern2, isInt2, err := parseArg(arg2, len(lines)) |
| 259 if err != nil { |
| 260 return "", "", "", err |
| 261 } |
| 262 if !isInt1 { |
| 263 line1, err = match(file, 0, lines, pattern1) |
| 264 } |
| 265 if !isInt2 { |
| 266 line2, err = match(file, line1, lines, pattern2) |
| 267 } else if line2 < line1 { |
| 268 return "", "", "", fmt.Errorf("lines out of order for %q: %d %d"
, file, line1, line2) |
| 269 } |
| 270 if err != nil { |
| 271 return "", "", "", err |
| 272 } |
| 273 for k := line1 - 1; k < line2; k++ { |
| 274 if strings.HasSuffix(lines[k], "OMIT\n") { |
| 275 lines[k] = "" |
| 276 } |
| 277 } |
| 278 return strings.Join(lines[line1-1:line2], ""), |
| 279 strings.Join(lines[:line1-1], ""), |
| 280 strings.Join(lines[line2:], ""), |
| 281 nil |
| 282 } |
| 283 |
| 284 // match identifies the input line that matches the pattern in a code invocation
. |
| 285 // If start>0, match lines starting there rather than at the beginning. |
| 286 // The return value is 1-indexed. |
| 287 func match(file string, start int, lines []string, pattern string) (int, error)
{ |
| 288 // $ matches the end of the file. |
| 289 if pattern == "$" { |
| 290 if len(lines) == 0 { |
| 291 return 0, fmt.Errorf("%q: empty file", file) |
| 292 } |
| 293 return len(lines), nil |
| 294 } |
| 295 // /regexp/ matches the line that matches the regexp. |
| 296 if len(pattern) > 2 && pattern[0] == '/' && pattern[len(pattern)-1] == '
/' { |
| 297 re, err := regexp.Compile(pattern[1 : len(pattern)-1]) |
| 298 if err != nil { |
| 299 return 0, err |
| 300 } |
| 301 for i := start; i < len(lines); i++ { |
| 302 if re.MatchString(lines[i]) { |
| 303 return i + 1, nil |
| 304 } |
| 305 } |
| 306 return 0, fmt.Errorf("%s: no match for %#q", file, pattern) |
| 307 } |
| 308 return 0, fmt.Errorf("unrecognized pattern: %q", pattern) |
| 309 } |
LEFT | RIGHT |