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.
4
5package present
6
7import (
8	"bufio"
9	"bytes"
10	"fmt"
11	"html/template"
12	"path/filepath"
13	"regexp"
14	"strconv"
15	"strings"
16)
17
18// PlayEnabled specifies whether runnable playground snippets should be
19// displayed in the present user interface.
20var PlayEnabled = false
21
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
25// templates.
26
27// NotesEnabled specifies whether presenter notes should be displayed in the
28// present user interface.
29var NotesEnabled = false
30
31func init() {
32	Register("code", parseCode)
33	Register("play", parseCode)
34}
35
36type Code struct {
37	Text     template.HTML
38	Play     bool   // runnable code
39	Edit     bool   // editable code
40	FileName string // file name
41	Ext      string // file extension
42	Raw      []byte // content of the file
43}
44
45func (c Code) TemplateName() string { return "code" }
46
47// The input line is a .code or .play entry with a file name and an optional HLfoo marker on the end.
48// Anything between the file and HL (if any) is an address expression, which we treat as a string here.
49// We pick off the HL first, for easy parsing.
50var (
51	highlightRE = regexp.MustCompile(`\s+HL([a-zA-Z0-9_]+)?$`)
52	hlCommentRE = regexp.MustCompile(`(.+) // HL(.*)$`)
53	codeRE      = regexp.MustCompile(`\.(code|play)\s+((?:(?:-edit|-numbers)\s+)*)([^\s]+)(?:\s+(.*))?$`)
54)
55
56// parseCode parses a code present directive. Its syntax:
57//   .code [-numbers] [-edit] <filename> [address] [highlight]
58// The directive may also be ".play" if the snippet is executable.
59func parseCode(ctx *Context, sourceFile string, sourceLine int, cmd string) (Elem, error) {
60	cmd = strings.TrimSpace(cmd)
61
62	// Pull off the HL, if any, from the end of the input line.
63	highlight := ""
64	if hl := highlightRE.FindStringSubmatchIndex(cmd); len(hl) == 4 {
65		if hl[2] < 0 || hl[3] < 0 {
66			return nil, fmt.Errorf("%s:%d invalid highlight syntax", sourceFile, sourceLine)
67		}
68		highlight = cmd[hl[2]:hl[3]]
69		cmd = cmd[:hl[2]-2]
70	}
71
72	// Parse the remaining command line.
73	// Arguments:
74	// args[0]: whole match
75	// args[1]:  .code/.play
76	// args[2]: flags ("-edit -numbers")
77	// args[3]: file name
78	// args[4]: optional address
79	args := codeRE.FindStringSubmatch(cmd)
80	if len(args) != 5 {
81		return nil, fmt.Errorf("%s:%d: syntax error for .code/.play invocation", sourceFile, sourceLine)
82	}
83	command, flags, file, addr := args[1], args[2], args[3], strings.TrimSpace(args[4])
84	play := command == "play" && PlayEnabled
85
86	// Read in code file and (optionally) match address.
87	filename := filepath.Join(filepath.Dir(sourceFile), file)
88	textBytes, err := ctx.ReadFile(filename)
89	if err != nil {
90		return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err)
91	}
92	lo, hi, err := addrToByteRange(addr, 0, textBytes)
93	if err != nil {
94		return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err)
95	}
96	if lo > hi {
97		// The search in addrToByteRange can wrap around so we might
98		// end up with the range ending before its starting point
99		hi, lo = lo, hi
100	}
101
102	// Acme pattern matches can stop mid-line,
103	// so run to end of line in both directions if not at line start/end.
104	for lo > 0 && textBytes[lo-1] != '\n' {
105		lo--
106	}
107	if hi > 0 {
108		for hi < len(textBytes) && textBytes[hi-1] != '\n' {
109			hi++
110		}
111	}
112
113	lines := codeLines(textBytes, lo, hi)
114
115	data := &codeTemplateData{
116		Lines:   formatLines(lines, highlight),
117		Edit:    strings.Contains(flags, "-edit"),
118		Numbers: strings.Contains(flags, "-numbers"),
119	}
120
121	// Include before and after in a hidden span for playground code.
122	if play {
123		data.Prefix = textBytes[:lo]
124		data.Suffix = textBytes[hi:]
125	}
126
127	var buf bytes.Buffer
128	if err := codeTemplate.Execute(&buf, data); err != nil {
129		return nil, err
130	}
131	return Code{
132		Text:     template.HTML(buf.String()),
133		Play:     play,
134		Edit:     data.Edit,
135		FileName: filepath.Base(filename),
136		Ext:      filepath.Ext(filename),
137		Raw:      rawCode(lines),
138	}, nil
139}
140
141// formatLines returns a new slice of codeLine with the given lines
142// replacing tabs with spaces and adding highlighting where needed.
143func formatLines(lines []codeLine, highlight string) []codeLine {
144	formatted := make([]codeLine, len(lines))
145	for i, line := range lines {
146		// Replace tabs with spaces, which work better in HTML.
147		line.L = strings.Replace(line.L, "\t", "    ", -1)
148
149		// Highlight lines that end with "// HL[highlight]"
150		// and strip the magic comment.
151		if m := hlCommentRE.FindStringSubmatch(line.L); m != nil {
152			line.L = m[1]
153			line.HL = m[2] == highlight
154		}
155
156		formatted[i] = line
157	}
158	return formatted
159}
160
161// rawCode returns the code represented by the given codeLines without any kind
162// of formatting.
163func rawCode(lines []codeLine) []byte {
164	b := new(bytes.Buffer)
165	for _, line := range lines {
166		b.WriteString(line.L)
167		b.WriteByte('\n')
168	}
169	return b.Bytes()
170}
171
172type codeTemplateData struct {
173	Lines          []codeLine
174	Prefix, Suffix []byte
175	Edit, Numbers  bool
176}
177
178var leadingSpaceRE = regexp.MustCompile(`^[ \t]*`)
179
180var codeTemplate = template.Must(template.New("code").Funcs(template.FuncMap{
181	"trimSpace":    strings.TrimSpace,
182	"leadingSpace": leadingSpaceRE.FindString,
183}).Parse(codeTemplateHTML))
184
185const codeTemplateHTML = `
186{{with .Prefix}}<pre style="display: none"><span>{{printf "%s" .}}</span></pre>{{end}}
187
188<pre{{if .Edit}} contenteditable="true" spellcheck="false"{{end}}{{if .Numbers}} class="numbers"{{end}}>{{/*
189	*/}}{{range .Lines}}<span num="{{.N}}">{{/*
190	*/}}{{if .HL}}{{leadingSpace .L}}<b>{{trimSpace .L}}</b>{{/*
191	*/}}{{else}}{{.L}}{{end}}{{/*
192*/}}</span>
193{{end}}</pre>
194
195{{with .Suffix}}<pre style="display: none"><span>{{printf "%s" .}}</span></pre>{{end}}
196`
197
198// codeLine represents a line of code extracted from a source file.
199type codeLine struct {
200	L  string // The line of code.
201	N  int    // The line number from the source file.
202	HL bool   // Whether the line should be highlighted.
203}
204
205// codeLines takes a source file and returns the lines that
206// span the byte range specified by start and end.
207// It discards lines that end in "OMIT".
208func codeLines(src []byte, start, end int) (lines []codeLine) {
209	startLine := 1
210	for i, b := range src {
211		if i == start {
212			break
213		}
214		if b == '\n' {
215			startLine++
216		}
217	}
218	s := bufio.NewScanner(bytes.NewReader(src[start:end]))
219	for n := startLine; s.Scan(); n++ {
220		l := s.Text()
221		if strings.HasSuffix(l, "OMIT") {
222			continue
223		}
224		lines = append(lines, codeLine{L: l, N: n})
225	}
226	// Trim leading and trailing blank lines.
227	for len(lines) > 0 && len(lines[0].L) == 0 {
228		lines = lines[1:]
229	}
230	for len(lines) > 0 && len(lines[len(lines)-1].L) == 0 {
231		lines = lines[:len(lines)-1]
232	}
233	return
234}
235
236func parseArgs(name string, line int, args []string) (res []interface{}, err error) {
237	res = make([]interface{}, len(args))
238	for i, v := range args {
239		if len(v) == 0 {
240			return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v)
241		}
242		switch v[0] {
243		case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
244			n, err := strconv.Atoi(v)
245			if err != nil {
246				return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v)
247			}
248			res[i] = n
249		case '/':
250			if len(v) < 2 || v[len(v)-1] != '/' {
251				return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v)
252			}
253			res[i] = v
254		case '$':
255			res[i] = "$"
256		case '_':
257			if len(v) == 1 {
258				// Do nothing; "_" indicates an intentionally empty parameter.
259				break
260			}
261			fallthrough
262		default:
263			return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v)
264		}
265	}
266	return
267}
268