You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

267 lines
7.6 KiB

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