| // Copyright 2012 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package present |
| |
| import ( |
| "bufio" |
| "bytes" |
| "fmt" |
| "html/template" |
| "path/filepath" |
| "regexp" |
| "strconv" |
| "strings" |
| ) |
| |
| // Is the playground available? |
| var PlayEnabled = false |
| |
| // TOOD(adg): replace the PlayEnabled flag with something less spaghetti-like. |
| // Instead this will probably be determined by a template execution Context |
| // value that contains various global metadata required when rendering |
| // templates. |
| |
| func init() { |
| Register("code", parseCode) |
| Register("play", parseCode) |
| } |
| |
| type Code struct { |
| Text template.HTML |
| Play bool // runnable code |
| } |
| |
| func (c Code) TemplateName() string { return "code" } |
| |
| // The input line is a .code or .play entry with a file name and an optional HLfoo marker on the end. |
| // Anything between the file and HL (if any) is an address expression, which we treat as a string here. |
| // We pick off the HL first, for easy parsing. |
| var ( |
| highlightRE = regexp.MustCompile(`\s+HL([a-zA-Z0-9_]+)?$`) |
| hlCommentRE = regexp.MustCompile(`(.+) // HL(.*)$`) |
| codeRE = regexp.MustCompile(`\.(code|play)\s+([^\s]+)(\s+)?(.*)?$`) |
| ) |
| |
| func parseCode(ctx *Context, sourceFile string, sourceLine int, cmd string) (Elem, error) { |
| cmd = strings.TrimSpace(cmd) |
| |
| // Pull off the HL, if any, from the end of the input line. |
| highlight := "" |
| if hl := highlightRE.FindStringSubmatchIndex(cmd); len(hl) == 4 { |
| highlight = cmd[hl[2]:hl[3]] |
| cmd = cmd[:hl[2]-2] |
| } |
| |
| // Parse the remaining command line. |
| // Arguments: |
| // args[0]: whole match |
| // args[1]: .code/.play |
| // args[2]: file name |
| // args[3]: space, if any, before optional address |
| // args[4]: optional address |
| args := codeRE.FindStringSubmatch(cmd) |
| if len(args) != 5 { |
| return nil, fmt.Errorf("%s:%d: syntax error for .code/.play invocation", sourceFile, sourceLine) |
| } |
| command, file, addr := args[1], args[2], strings.TrimSpace(args[4]) |
| play := command == "play" && PlayEnabled |
| |
| // Read in code file and (optionally) match address. |
| filename := filepath.Join(filepath.Dir(sourceFile), file) |
| textBytes, err := ctx.ReadFile(filename) |
| if err != nil { |
| return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err) |
| } |
| lo, hi, err := addrToByteRange(addr, 0, textBytes) |
| if err != nil { |
| return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err) |
| } |
| |
| // Acme pattern matches can stop mid-line, |
| // so run to end of line in both directions if not at line start/end. |
| for lo > 0 && textBytes[lo-1] != '\n' { |
| lo-- |
| } |
| if hi > 0 { |
| for hi < len(textBytes) && textBytes[hi-1] != '\n' { |
| hi++ |
| } |
| } |
| |
| lines := codeLines(textBytes, lo, hi) |
| |
| for i, line := range lines { |
| // Replace tabs by spaces, which work better in HTML. |
| line.L = strings.Replace(line.L, "\t", " ", -1) |
| |
| // Highlight lines that end with "// HL[highlight]" |
| // and strip the magic comment. |
| if m := hlCommentRE.FindStringSubmatch(line.L); m != nil { |
| line.L = m[1] |
| line.HL = m[2] == highlight |
| } |
| |
| lines[i] = line |
| } |
| |
| data := &codeTemplateData{Lines: lines} |
| |
| // Include before and after in a hidden span for playground code. |
| if play { |
| data.Prefix = textBytes[:lo] |
| data.Suffix = textBytes[hi:] |
| } |
| |
| var buf bytes.Buffer |
| if err := codeTemplate.Execute(&buf, data); err != nil { |
| return nil, err |
| } |
| return Code{Text: template.HTML(buf.String()), Play: play}, nil |
| } |
| |
| type codeTemplateData struct { |
| Lines []codeLine |
| Prefix, Suffix []byte |
| } |
| |
| var leadingSpaceRE = regexp.MustCompile(`^[ \t]*`) |
| |
| var codeTemplate = template.Must(template.New("code").Funcs(template.FuncMap{ |
| "trimSpace": strings.TrimSpace, |
| "leadingSpace": leadingSpaceRE.FindString, |
| }).Parse(codeTemplateHTML)) |
| |
| const codeTemplateHTML = ` |
| {{with .Prefix}}<pre style="display: none"><span>{{printf "%s" .}}</span></pre>{{end}} |
| |
| <pre>{{range .Lines}}<span num="{{.N}}">{{/* |
| */}}{{if .HL}}{{leadingSpace .L}}<b>{{trimSpace .L}}</b>{{/* |
| */}}{{else}}{{.L}}{{end}}{{/* |
| */}}</span> |
| {{end}}</pre> |
| |
| {{with .Suffix}}<pre style="display: none"><span>{{printf "%s" .}}</span></pre>{{end}} |
| ` |
| |
| // codeLine represents a line of code extracted from a source file. |
| type codeLine struct { |
| L string // The line of code. |
| N int // The line number from the source file. |
| HL bool // Whether the line should be highlighted. |
| } |
| |
| // codeLines takes a source file and returns the lines that |
| // span the byte range specified by start and end. |
| // It discards lines that end in "OMIT". |
| func codeLines(src []byte, start, end int) (lines []codeLine) { |
| startLine := 1 |
| for i, b := range src { |
| if i == start { |
| break |
| } |
| if b == '\n' { |
| startLine++ |
| } |
| } |
| s := bufio.NewScanner(bytes.NewReader(src[start:end])) |
| for n := startLine; s.Scan(); n++ { |
| l := s.Text() |
| if strings.HasSuffix(l, "OMIT") { |
| continue |
| } |
| lines = append(lines, codeLine{L: l, N: n}) |
| } |
| // Trim leading and trailing blank lines. |
| for len(lines) > 0 && len(lines[0].L) == 0 { |
| lines = lines[1:] |
| } |
| for len(lines) > 0 && len(lines[len(lines)-1].L) == 0 { |
| lines = lines[:len(lines)-1] |
| } |
| return |
| } |
| |
| func parseArgs(name string, line int, args []string) (res []interface{}, err error) { |
| res = make([]interface{}, len(args)) |
| for i, v := range args { |
| if len(v) == 0 { |
| return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v) |
| } |
| switch v[0] { |
| case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': |
| n, err := strconv.Atoi(v) |
| if err != nil { |
| return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v) |
| } |
| res[i] = n |
| case '/': |
| if len(v) < 2 || v[len(v)-1] != '/' { |
| return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v) |
| } |
| res[i] = v |
| case '$': |
| res[i] = "$" |
| default: |
| return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v) |
| } |
| } |
| return |
| } |
| |
| // parseArg returns the integer or string value of the argument and tells which it is. |
| func parseArg(arg interface{}, max int) (ival int, sval string, isInt bool, err error) { |
| switch n := arg.(type) { |
| case int: |
| if n <= 0 || n > max { |
| return 0, "", false, fmt.Errorf("%d is out of range", n) |
| } |
| return n, "", true, nil |
| case string: |
| return 0, n, false, nil |
| } |
| return 0, "", false, fmt.Errorf("unrecognized argument %v type %T", arg, arg) |
| } |
| |
| // match identifies the input line that matches the pattern in a code invocation. |
| // If start>0, match lines starting there rather than at the beginning. |
| // The return value is 1-indexed. |
| func match(file string, start int, lines []string, pattern string) (int, error) { |
| // $ matches the end of the file. |
| if pattern == "$" { |
| if len(lines) == 0 { |
| return 0, fmt.Errorf("%q: empty file", file) |
| } |
| return len(lines), nil |
| } |
| // /regexp/ matches the line that matches the regexp. |
| if len(pattern) > 2 && pattern[0] == '/' && pattern[len(pattern)-1] == '/' { |
| re, err := regexp.Compile(pattern[1 : len(pattern)-1]) |
| if err != nil { |
| return 0, err |
| } |
| for i := start; i < len(lines); i++ { |
| if re.MatchString(lines[i]) { |
| return i + 1, nil |
| } |
| } |
| return 0, fmt.Errorf("%s: no match for %#q", file, pattern) |
| } |
| return 0, fmt.Errorf("unrecognized pattern: %q", pattern) |
| } |