1 // Copyright 2012 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
18 // PlayEnabled specifies whether runnable playground snippets should be
19 // displayed in the present user interface.
20 var PlayEnabled = false
22 // TODO(adg): replace the PlayEnabled flag with something less spaghetti-like.
23 // Instead this will probably be determined by a template execution Context
24 // value that contains various global metadata required when rendering
27 // NotesEnabled specifies whether presenter notes should be displayed in the
28 // present user interface.
29 var NotesEnabled = false
32 Register("code", parseCode)
33 Register("play", parseCode)
37 Cmd string // original command from present source
39 Play bool // runnable code
40 Edit bool // editable code
41 FileName string // file name
42 Ext string // file extension
43 Raw []byte // content of the file
46 func (c Code) PresentCmd() string { return c.Cmd }
47 func (c Code) TemplateName() string { return "code" }
49 // The input line is a .code or .play entry with a file name and an optional HLfoo marker on the end.
50 // Anything between the file and HL (if any) is an address expression, which we treat as a string here.
51 // We pick off the HL first, for easy parsing.
53 highlightRE = regexp.MustCompile(`\s+HL([a-zA-Z0-9_]+)?$`)
54 hlCommentRE = regexp.MustCompile(`(.+) // HL(.*)$`)
55 codeRE = regexp.MustCompile(`\.(code|play)\s+((?:(?:-edit|-numbers)\s+)*)([^\s]+)(?:\s+(.*))?$`)
58 // parseCode parses a code present directive. Its syntax:
59 // .code [-numbers] [-edit] <filename> [address] [highlight]
60 // The directive may also be ".play" if the snippet is executable.
61 func parseCode(ctx *Context, sourceFile string, sourceLine int, cmd string) (Elem, error) {
62 cmd = strings.TrimSpace(cmd)
65 // Pull off the HL, if any, from the end of the input line.
67 if hl := highlightRE.FindStringSubmatchIndex(cmd); len(hl) == 4 {
68 if hl[2] < 0 || hl[3] < 0 {
69 return nil, fmt.Errorf("%s:%d invalid highlight syntax", sourceFile, sourceLine)
71 highlight = cmd[hl[2]:hl[3]]
75 // Parse the remaining command line.
77 // args[0]: whole match
78 // args[1]: .code/.play
79 // args[2]: flags ("-edit -numbers")
81 // args[4]: optional address
82 args := codeRE.FindStringSubmatch(cmd)
84 return nil, fmt.Errorf("%s:%d: syntax error for .code/.play invocation", sourceFile, sourceLine)
86 command, flags, file, addr := args[1], args[2], args[3], strings.TrimSpace(args[4])
87 play := command == "play" && PlayEnabled
89 // Read in code file and (optionally) match address.
90 filename := filepath.Join(filepath.Dir(sourceFile), file)
91 textBytes, err := ctx.ReadFile(filename)
93 return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err)
95 lo, hi, err := addrToByteRange(addr, 0, textBytes)
97 return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err)
100 // The search in addrToByteRange can wrap around so we might
101 // end up with the range ending before its starting point
105 // Acme pattern matches can stop mid-line,
106 // so run to end of line in both directions if not at line start/end.
107 for lo > 0 && textBytes[lo-1] != '\n' {
111 for hi < len(textBytes) && textBytes[hi-1] != '\n' {
116 lines := codeLines(textBytes, lo, hi)
118 data := &codeTemplateData{
119 Lines: formatLines(lines, highlight),
120 Edit: strings.Contains(flags, "-edit"),
121 Numbers: strings.Contains(flags, "-numbers"),
124 // Include before and after in a hidden span for playground code.
126 data.Prefix = textBytes[:lo]
127 data.Suffix = textBytes[hi:]
131 if err := codeTemplate.Execute(&buf, data); err != nil {
136 Text: template.HTML(buf.String()),
139 FileName: filepath.Base(filename),
140 Ext: filepath.Ext(filename),
145 // formatLines returns a new slice of codeLine with the given lines
146 // replacing tabs with spaces and adding highlighting where needed.
147 func formatLines(lines []codeLine, highlight string) []codeLine {
148 formatted := make([]codeLine, len(lines))
149 for i, line := range lines {
150 // Replace tabs with spaces, which work better in HTML.
151 line.L = strings.Replace(line.L, "\t", " ", -1)
153 // Highlight lines that end with "// HL[highlight]"
154 // and strip the magic comment.
155 if m := hlCommentRE.FindStringSubmatch(line.L); m != nil {
157 line.HL = m[2] == highlight
165 // rawCode returns the code represented by the given codeLines without any kind
167 func rawCode(lines []codeLine) []byte {
168 b := new(bytes.Buffer)
169 for _, line := range lines {
170 b.WriteString(line.L)
176 type codeTemplateData struct {
178 Prefix, Suffix []byte
182 var leadingSpaceRE = regexp.MustCompile(`^[ \t]*`)
184 var codeTemplate = template.Must(template.New("code").Funcs(template.FuncMap{
185 "trimSpace": strings.TrimSpace,
186 "leadingSpace": leadingSpaceRE.FindString,
187 }).Parse(codeTemplateHTML))
189 const codeTemplateHTML = `
190 {{with .Prefix}}<pre style="display: none"><span>{{printf "%s" .}}</span></pre>{{end -}}
192 <pre{{if .Edit}} contenteditable="true" spellcheck="false"{{end}}{{if .Numbers}} class="numbers"{{end}}>{{/*
193 */}}{{range .Lines}}<span num="{{.N}}">{{/*
194 */}}{{if .HL}}{{leadingSpace .L}}<b>{{trimSpace .L}}</b>{{/*
195 */}}{{else}}{{.L}}{{end}}{{/*
198 {{with .Suffix}}<pre style="display: none"><span>{{printf "%s" .}}</span></pre>{{end -}}
201 // codeLine represents a line of code extracted from a source file.
202 type codeLine struct {
203 L string // The line of code.
204 N int // The line number from the source file.
205 HL bool // Whether the line should be highlighted.
208 // codeLines takes a source file and returns the lines that
209 // span the byte range specified by start and end.
210 // It discards lines that end in "OMIT".
211 func codeLines(src []byte, start, end int) (lines []codeLine) {
213 for i, b := range src {
221 s := bufio.NewScanner(bytes.NewReader(src[start:end]))
222 for n := startLine; s.Scan(); n++ {
224 if strings.HasSuffix(l, "OMIT") {
227 lines = append(lines, codeLine{L: l, N: n})
229 // Trim leading and trailing blank lines.
230 for len(lines) > 0 && len(lines[0].L) == 0 {
233 for len(lines) > 0 && len(lines[len(lines)-1].L) == 0 {
234 lines = lines[:len(lines)-1]
239 func parseArgs(name string, line int, args []string) (res []interface{}, err error) {
240 res = make([]interface{}, len(args))
241 for i, v := range args {
243 return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v)
246 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
247 n, err := strconv.Atoi(v)
249 return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v)
253 if len(v) < 2 || v[len(v)-1] != '/' {
254 return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v)
261 // Do nothing; "_" indicates an intentionally empty parameter.
266 return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v)