blob: 4d82d9aed56420f9e54b0c1453f72f8014db4e17 [file] [log] [blame]
// 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)
}