blob: c60aedf96709f3064321c594a46ba6316f436b16 [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 main
import (
"fmt"
"html/template"
"io/ioutil"
"log"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"unicode"
)
var playEnabled = false // to be enabled by a flag in main.go
func init() {
Register("code", parseCode, code)
Register("play", parseCode, code)
}
type Code struct {
Play bool // runnable code
File string // file name to read input from
Cmd string // text of input line
Addr string // really an address
Highlight string // HLxxx marker on end of line.
Type string // type extension of file (.go etc.).
SourceFile string
SourceLine int
}
func (c Code) HTML(t *template.Template) (template.HTML, error) {
return execTemplate(t, "code", c)
}
// 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_]+)?$`)
var codeRE = regexp.MustCompile(`\.(code|play)\s+([^\s]+)(\s+)?(.*)?$`)
func parseCode(fileName string, lineno int, text string) (Elem, error) {
text = strings.TrimSpace(text)
// Pull off the HL, if any, from the end of the input line.
highlight := ""
if hl := highlightRE.FindStringSubmatchIndex(text); len(hl) == 4 {
highlight = text[hl[2]:hl[3]]
text = text[:hl[2]-2]
}
// Parse the remaining command line.
args := codeRE.FindStringSubmatch(text)
// 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
if len(args) != 5 {
return nil, fmt.Errorf("%s:%d: syntax error for .code/.play invocation", fileName, lineno)
}
command, file, addr := args[1], args[2], strings.TrimSpace(args[4])
typ := path.Ext(fileName)
for len(typ) > 0 && typ[0] == '.' {
typ = typ[1:]
}
return Code{
Play: command == "play" && playEnabled,
File: file,
Cmd: text,
Addr: addr,
Highlight: highlight,
Type: typ,
SourceFile: fileName,
SourceLine: lineno}, nil
}
// contents reads a file by name and returns its contents as a byte slice.
func contents(name string) ([]byte, error) {
file, err := ioutil.ReadFile(name)
if err != nil {
return nil, err
}
return file, nil
}
// code is the entry point for the '.code' present command.
func code(c Code) (template.HTML, error) {
filename := filepath.Join(filepath.Dir(c.SourceFile), c.File)
textBytes, err := contents(filename)
if err != nil {
return "", fmt.Errorf("%s:%d: %v", c.SourceFile, c.SourceLine, err)
}
lo, hi, err := addrToByteRange(c.Addr, 0, textBytes)
if err != nil {
return "", fmt.Errorf("%s:%d: %v", c.SourceFile, c.SourceLine, err)
}
// Acme patterns stop mid-line, so run to end of line in both directions.
for lo > 0 && textBytes[lo-1] != '\n' {
lo--
}
for hi < len(textBytes) {
hi++
if textBytes[hi-1] == '\n' {
break
}
}
text := skipOMIT(textBytes[lo:hi])
// Replace tabs by spaces, which work better in HTML.
text = strings.Replace(text, "\t", " ", -1)
// Escape the program text for HTML.
text = template.HTMLEscapeString(text)
// Highlight and span-wrap lines.
text = "<pre>" + highlightLines(text, c.Highlight) + "</pre>"
// Include before and after in a hidden span for playground code.
if c.Play {
text = hide(skipOMIT(textBytes[:lo])) + text + hide(skipOMIT(textBytes[hi:]))
}
// Include the command as a comment.
text = fmt.Sprintf("<!--{{%s}}\n-->%s", c.Cmd, text)
return template.HTML(text), nil
}
// skipOMIT turns text into a string, dropping lines ending with OMIT.
func skipOMIT(text []byte) string {
lines := strings.SplitAfter(string(text), "\n")
for k := range lines {
if strings.HasSuffix(lines[k], "OMIT\n") {
lines[k] = ""
}
}
return strings.Join(lines, "")
}
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{}, file string, max int) (ival int, sval string, isInt bool) {
switch n := arg.(type) {
case int:
if n <= 0 || n > max {
log.Fatalf("%q:%d is out of range", file, n)
}
return n, "", true
case string:
return 0, n, false
}
log.Fatalf("unrecognized argument %v type %T", arg, arg)
return
}
// oneLine returns the single line generated by a two-argument code invocation.
func oneLine(file, text string, arg interface{}) (line, before, after string, err error) {
contentBytes, err := contents(file)
if err != nil {
return "", "", "", err
}
lines := strings.SplitAfter(string(contentBytes), "\n")
lineNum, pattern, isInt := parseArg(arg, file, len(lines))
var n int
if isInt {
n = lineNum - 1
} else {
n, err = match(file, 0, lines, pattern)
n -= 1
}
if err != nil {
return "", "", "", err
}
return lines[n],
strings.Join(lines[:n], ""),
strings.Join(lines[n+1:], ""),
nil
}
// multipleLines returns the text generated by a three-argument code invocation.
func multipleLines(file string, arg1, arg2 interface{}) (line, before, after string, err error) {
contentBytes, err := contents(file)
lines := strings.SplitAfter(string(contentBytes), "\n")
if err != nil {
return "", "", "", err
}
line1, pattern1, isInt1 := parseArg(arg1, file, len(lines))
line2, pattern2, isInt2 := parseArg(arg2, file, len(lines))
if !isInt1 {
line1, err = match(file, 0, lines, pattern1)
}
if !isInt2 {
line2, err = match(file, line1, lines, pattern2)
} else if line2 < line1 {
return "", "", "", fmt.Errorf("lines out of order for %q: %d %d", file, line1, line2)
}
if err != nil {
return "", "", "", err
}
for k := line1 - 1; k < line2; k++ {
if strings.HasSuffix(lines[k], "OMIT\n") {
lines[k] = ""
}
}
return strings.Join(lines[line1-1:line2], ""),
strings.Join(lines[:line1-1], ""),
strings.Join(lines[line2:], ""),
nil
}
// 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)
}
var hlRE = regexp.MustCompile(`(.+) // HL(.*)$`)
// highlightLines emboldens lines that end with "// HL" and
// wraps any other lines in span tags.
func highlightLines(text, label string) string {
lines := strings.Split(text, "\n")
for i, line := range lines {
m := hlRE.FindStringSubmatch(line)
if m == nil {
continue
}
line := m[1]
space := ""
if j := strings.IndexFunc(line, func(r rune) bool {
return !unicode.IsSpace(r)
}); j > 0 {
space = line[:j]
line = line[j:]
}
if m[2] == "" || m[2] == label {
lines[i] = space + "<b>" + line + "</b>"
}
}
return strings.Join(lines, "\n")
}
func hide(text string) string {
return fmt.Sprintf(`<pre style="display: none">%s</pre>`, template.HTMLEscapeString(text))
}