github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/present/code.go (about) 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 // .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) 63 origCmd := cmd 64 65 // Pull off the HL, if any, from the end of the input line. 66 highlight := "" 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) 70 } 71 highlight = cmd[hl[2]:hl[3]] 72 cmd = cmd[:hl[2]-2] 73 } 74 75 // Parse the remaining command line. 76 // Arguments: 77 // args[0]: whole match 78 // args[1]: .code/.play 79 // args[2]: flags ("-edit -numbers") 80 // args[3]: file name 81 // args[4]: optional address 82 args := codeRE.FindStringSubmatch(cmd) 83 if len(args) != 5 { 84 return nil, fmt.Errorf("%s:%d: syntax error for .code/.play invocation", sourceFile, sourceLine) 85 } 86 command, flags, file, addr := args[1], args[2], args[3], strings.TrimSpace(args[4]) 87 play := command == "play" && PlayEnabled 88 89 // Read in code file and (optionally) match address. 90 filename := filepath.Join(filepath.Dir(sourceFile), file) 91 textBytes, err := ctx.ReadFile(filename) 92 if err != nil { 93 return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err) 94 } 95 lo, hi, err := addrToByteRange(addr, 0, textBytes) 96 if err != nil { 97 return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err) 98 } 99 if lo > hi { 100 // The search in addrToByteRange can wrap around so we might 101 // end up with the range ending before its starting point 102 hi, lo = lo, hi 103 } 104 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' { 108 lo-- 109 } 110 if hi > 0 { 111 for hi < len(textBytes) && textBytes[hi-1] != '\n' { 112 hi++ 113 } 114 } 115 116 lines := codeLines(textBytes, lo, hi) 117 118 data := &codeTemplateData{ 119 Lines: formatLines(lines, highlight), 120 Edit: strings.Contains(flags, "-edit"), 121 Numbers: strings.Contains(flags, "-numbers"), 122 } 123 124 // Include before and after in a hidden span for playground code. 125 if play { 126 data.Prefix = textBytes[:lo] 127 data.Suffix = textBytes[hi:] 128 } 129 130 var buf bytes.Buffer 131 if err := codeTemplate.Execute(&buf, data); err != nil { 132 return nil, err 133 } 134 return Code{ 135 Cmd: origCmd, 136 Text: template.HTML(buf.String()), 137 Play: play, 138 Edit: data.Edit, 139 FileName: filepath.Base(filename), 140 Ext: filepath.Ext(filename), 141 Raw: rawCode(lines), 142 }, nil 143 } 144 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) 152 153 // Highlight lines that end with "// HL[highlight]" 154 // and strip the magic comment. 155 if m := hlCommentRE.FindStringSubmatch(line.L); m != nil { 156 line.L = m[1] 157 line.HL = m[2] == highlight 158 } 159 160 formatted[i] = line 161 } 162 return formatted 163 } 164 165 // rawCode returns the code represented by the given codeLines without any kind 166 // of formatting. 167 func rawCode(lines []codeLine) []byte { 168 b := new(bytes.Buffer) 169 for _, line := range lines { 170 b.WriteString(line.L) 171 b.WriteByte('\n') 172 } 173 return b.Bytes() 174 } 175 176 type codeTemplateData struct { 177 Lines []codeLine 178 Prefix, Suffix []byte 179 Edit, Numbers bool 180 } 181 182 var leadingSpaceRE = regexp.MustCompile(`^[ \t]*`) 183 184 var codeTemplate = template.Must(template.New("code").Funcs(template.FuncMap{ 185 "trimSpace": strings.TrimSpace, 186 "leadingSpace": leadingSpaceRE.FindString, 187 }).Parse(codeTemplateHTML)) 188 189 const codeTemplateHTML = ` 190 {{with .Prefix}}<pre style="display: none"><span>{{printf "%s" .}}</span></pre>{{end -}} 191 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}}{{/* 196 */}}</span> 197 {{end}}</pre> 198 {{with .Suffix}}<pre style="display: none"><span>{{printf "%s" .}}</span></pre>{{end -}} 199 ` 200 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. 206 } 207 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) { 212 startLine := 1 213 for i, b := range src { 214 if i == start { 215 break 216 } 217 if b == '\n' { 218 startLine++ 219 } 220 } 221 s := bufio.NewScanner(bytes.NewReader(src[start:end])) 222 for n := startLine; s.Scan(); n++ { 223 l := s.Text() 224 if strings.HasSuffix(l, "OMIT") { 225 continue 226 } 227 lines = append(lines, codeLine{L: l, N: n}) 228 } 229 // Trim leading and trailing blank lines. 230 for len(lines) > 0 && len(lines[0].L) == 0 { 231 lines = lines[1:] 232 } 233 for len(lines) > 0 && len(lines[len(lines)-1].L) == 0 { 234 lines = lines[:len(lines)-1] 235 } 236 return 237 } 238 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 { 242 if len(v) == 0 { 243 return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v) 244 } 245 switch v[0] { 246 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 247 n, err := strconv.Atoi(v) 248 if err != nil { 249 return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v) 250 } 251 res[i] = n 252 case '/': 253 if len(v) < 2 || v[len(v)-1] != '/' { 254 return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v) 255 } 256 res[i] = v 257 case '$': 258 res[i] = "$" 259 case '_': 260 if len(v) == 1 { 261 // Do nothing; "_" indicates an intentionally empty parameter. 262 break 263 } 264 fallthrough 265 default: 266 return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v) 267 } 268 } 269 return 270 }