1
2
3
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
19
20 var PlayEnabled = false
21
22
23
24
25
26
27
28
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
38 Text template.HTML
39 Play bool
40 Edit bool
41 FileName string
42 Ext string
43 Raw []byte
44 }
45
46 func (c Code) PresentCmd() string { return c.Cmd }
47 func (c Code) TemplateName() string { return "code" }
48
49
50
51
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
59
60
61
62
63 func parseCode(ctx *Context, sourceFile string, sourceLine int, cmd string) (Elem, error) {
64 cmd = strings.TrimSpace(cmd)
65 origCmd := cmd
66
67
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
78
79
80
81
82
83
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
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
103
104 hi, lo = lo, hi
105 }
106
107
108
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
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
148
149 func formatLines(lines []codeLine, highlight string) []codeLine {
150 formatted := make([]codeLine, len(lines))
151 for i, line := range lines {
152
153 line.L = strings.Replace(line.L, "\t", " ", -1)
154
155
156
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
168
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
204 type codeLine struct {
205 L string
206 N int
207 HL bool
208 }
209
210
211
212
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
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
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
View as plain text