// 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}}
{{printf "%s" .}}
{{end}}
{{range .Lines}}{{/*
	*/}}{{if .HL}}{{leadingSpace .L}}{{trimSpace .L}}{{/*
	*/}}{{else}}{{.L}}{{end}}{{/*
*/}}
{{end}}
{{with .Suffix}}
{{printf "%s" .}}
{{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) }