1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
|
// 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"
)
// PlayEnabled specifies whether runnable playground snippets should be
// displayed in the present user interface.
var PlayEnabled = false
// TODO(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.
// NotesEnabled specifies whether presenter notes should be displayed in the
// present user interface.
var NotesEnabled = false
func init() {
Register("code", parseCode)
Register("play", parseCode)
}
type Code struct {
Cmd string // original command from present source
Text template.HTML
Play bool // runnable code
Edit bool // editable code
FileName string // file name
Ext string // file extension
Raw []byte // content of the file
}
func (c Code) PresentCmd() string { return c.Cmd }
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+((?:(?:-edit|-numbers)\s+)*)([^\s]+)(?:\s+(.*))?$`)
)
// parseCode parses a code present directive. Its syntax:
//
// .code [-numbers] [-edit] <filename> [address] [highlight]
//
// The directive may also be ".play" if the snippet is executable.
func parseCode(ctx *Context, sourceFile string, sourceLine int, cmd string) (Elem, error) {
cmd = strings.TrimSpace(cmd)
origCmd := cmd
// Pull off the HL, if any, from the end of the input line.
highlight := ""
if hl := highlightRE.FindStringSubmatchIndex(cmd); len(hl) == 4 {
if hl[2] < 0 || hl[3] < 0 {
return nil, fmt.Errorf("%s:%d invalid highlight syntax", sourceFile, sourceLine)
}
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]: flags ("-edit -numbers")
// args[3]: file name
// 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, flags, file, addr := args[1], args[2], args[3], 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)
}
if lo > hi {
// The search in addrToByteRange can wrap around so we might
// end up with the range ending before its starting point
hi, lo = lo, hi
}
// 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)
data := &codeTemplateData{
Lines: formatLines(lines, highlight),
Edit: strings.Contains(flags, "-edit"),
Numbers: strings.Contains(flags, "-numbers"),
}
// 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{
Cmd: origCmd,
Text: template.HTML(buf.String()),
Play: play,
Edit: data.Edit,
FileName: filepath.Base(filename),
Ext: filepath.Ext(filename),
Raw: rawCode(lines),
}, nil
}
// formatLines returns a new slice of codeLine with the given lines
// replacing tabs with spaces and adding highlighting where needed.
func formatLines(lines []codeLine, highlight string) []codeLine {
formatted := make([]codeLine, len(lines))
for i, line := range lines {
// Replace tabs with 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
}
formatted[i] = line
}
return formatted
}
// rawCode returns the code represented by the given codeLines without any kind
// of formatting.
func rawCode(lines []codeLine) []byte {
b := new(bytes.Buffer)
for _, line := range lines {
b.WriteString(line.L)
b.WriteByte('\n')
}
return b.Bytes()
}
type codeTemplateData struct {
Lines []codeLine
Prefix, Suffix []byte
Edit, Numbers bool
}
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{{if .Edit}} contenteditable="true" spellcheck="false"{{end}}{{if .Numbers}} class="numbers"{{end}}>{{/*
*/}}{{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] = "$"
case '_':
if len(v) == 1 {
// Do nothing; "_" indicates an intentionally empty parameter.
break
}
fallthrough
default:
return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v)
}
}
return
}
|