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