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.

179 lines
5.4 KiB

  1. // Copyright 2011 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. // Template support for writing HTML documents.
  5. // Documents that include Template: true in their
  6. // metadata are executed as input to text/template.
  7. //
  8. // This file defines functions for those templates to invoke.
  9. // The template uses the function "code" to inject program
  10. // source into the output by extracting code from files and
  11. // injecting them as HTML-escaped <pre> blocks.
  12. //
  13. // The syntax is simple: 1, 2, or 3 space-separated arguments:
  14. //
  15. // Whole file:
  16. // {{code "foo.go"}}
  17. // One line (here the signature of main):
  18. // {{code "foo.go" `/^func.main/`}}
  19. // Block of text, determined by start and end (here the body of main):
  20. // {{code "foo.go" `/^func.main/` `/^}/`
  21. //
  22. // Patterns can be `/regular expression/`, a decimal number, or "$"
  23. // to signify the end of the file. In multi-line matches,
  24. // lines that end with the four characters
  25. // OMIT
  26. // are omitted from the output, making it easy to provide marker
  27. // lines in the input that will not appear in the output but are easy
  28. // to identify by pattern.
  29. package godoc
  30. import (
  31. "bytes"
  32. "fmt"
  33. "log"
  34. "regexp"
  35. "strings"
  36. "golang.org/x/tools/godoc/vfs"
  37. )
  38. // Functions in this file panic on error, but the panic is recovered
  39. // to an error by 'code'.
  40. // contents reads and returns the content of the named file
  41. // (from the virtual file system, so for example /doc refers to $GOROOT/doc).
  42. func (c *Corpus) contents(name string) string {
  43. file, err := vfs.ReadFile(c.fs, name)
  44. if err != nil {
  45. log.Panic(err)
  46. }
  47. return string(file)
  48. }
  49. // stringFor returns a textual representation of the arg, formatted according to its nature.
  50. func stringFor(arg interface{}) string {
  51. switch arg := arg.(type) {
  52. case int:
  53. return fmt.Sprintf("%d", arg)
  54. case string:
  55. if len(arg) > 2 && arg[0] == '/' && arg[len(arg)-1] == '/' {
  56. return fmt.Sprintf("%#q", arg)
  57. }
  58. return fmt.Sprintf("%q", arg)
  59. default:
  60. log.Panicf("unrecognized argument: %v type %T", arg, arg)
  61. }
  62. return ""
  63. }
  64. func (p *Presentation) code(file string, arg ...interface{}) (s string, err error) {
  65. defer func() {
  66. if r := recover(); r != nil {
  67. err = fmt.Errorf("%v", r)
  68. }
  69. }()
  70. text := p.Corpus.contents(file)
  71. var command string
  72. switch len(arg) {
  73. case 0:
  74. // text is already whole file.
  75. command = fmt.Sprintf("code %q", file)
  76. case 1:
  77. command = fmt.Sprintf("code %q %s", file, stringFor(arg[0]))
  78. text = p.Corpus.oneLine(file, text, arg[0])
  79. case 2:
  80. command = fmt.Sprintf("code %q %s %s", file, stringFor(arg[0]), stringFor(arg[1]))
  81. text = p.Corpus.multipleLines(file, text, arg[0], arg[1])
  82. default:
  83. return "", fmt.Errorf("incorrect code invocation: code %q %q", file, arg)
  84. }
  85. // Trim spaces from output.
  86. text = strings.Trim(text, "\n")
  87. // Replace tabs by spaces, which work better in HTML.
  88. text = strings.Replace(text, "\t", " ", -1)
  89. var buf bytes.Buffer
  90. // HTML-escape text and syntax-color comments like elsewhere.
  91. FormatText(&buf, []byte(text), -1, true, "", nil)
  92. // Include the command as a comment.
  93. text = fmt.Sprintf("<pre><!--{{%s}}\n-->%s</pre>", command, buf.Bytes())
  94. return text, nil
  95. }
  96. // parseArg returns the integer or string value of the argument and tells which it is.
  97. func parseArg(arg interface{}, file string, max int) (ival int, sval string, isInt bool) {
  98. switch n := arg.(type) {
  99. case int:
  100. if n <= 0 || n > max {
  101. log.Panicf("%q:%d is out of range", file, n)
  102. }
  103. return n, "", true
  104. case string:
  105. return 0, n, false
  106. }
  107. log.Panicf("unrecognized argument %v type %T", arg, arg)
  108. return
  109. }
  110. // oneLine returns the single line generated by a two-argument code invocation.
  111. func (c *Corpus) oneLine(file, text string, arg interface{}) string {
  112. lines := strings.SplitAfter(c.contents(file), "\n")
  113. line, pattern, isInt := parseArg(arg, file, len(lines))
  114. if isInt {
  115. return lines[line-1]
  116. }
  117. return lines[match(file, 0, lines, pattern)-1]
  118. }
  119. // multipleLines returns the text generated by a three-argument code invocation.
  120. func (c *Corpus) multipleLines(file, text string, arg1, arg2 interface{}) string {
  121. lines := strings.SplitAfter(c.contents(file), "\n")
  122. line1, pattern1, isInt1 := parseArg(arg1, file, len(lines))
  123. line2, pattern2, isInt2 := parseArg(arg2, file, len(lines))
  124. if !isInt1 {
  125. line1 = match(file, 0, lines, pattern1)
  126. }
  127. if !isInt2 {
  128. line2 = match(file, line1, lines, pattern2)
  129. } else if line2 < line1 {
  130. log.Panicf("lines out of order for %q: %d %d", text, line1, line2)
  131. }
  132. for k := line1 - 1; k < line2; k++ {
  133. if strings.HasSuffix(lines[k], "OMIT\n") {
  134. lines[k] = ""
  135. }
  136. }
  137. return strings.Join(lines[line1-1:line2], "")
  138. }
  139. // match identifies the input line that matches the pattern in a code invocation.
  140. // If start>0, match lines starting there rather than at the beginning.
  141. // The return value is 1-indexed.
  142. func match(file string, start int, lines []string, pattern string) int {
  143. // $ matches the end of the file.
  144. if pattern == "$" {
  145. if len(lines) == 0 {
  146. log.Panicf("%q: empty file", file)
  147. }
  148. return len(lines)
  149. }
  150. // /regexp/ matches the line that matches the regexp.
  151. if len(pattern) > 2 && pattern[0] == '/' && pattern[len(pattern)-1] == '/' {
  152. re, err := regexp.Compile(pattern[1 : len(pattern)-1])
  153. if err != nil {
  154. log.Panic(err)
  155. }
  156. for i := start; i < len(lines); i++ {
  157. if re.MatchString(lines[i]) {
  158. return i + 1
  159. }
  160. }
  161. log.Panicf("%s: no match for %#q", file, pattern)
  162. }
  163. log.Panicf("unrecognized pattern: %q", pattern)
  164. return 0
  165. }