github.com/cockroachdb/tools@v0.0.0-20230222021103-a6d27438930d/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 // 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 }