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