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
32package godoc
33
34import (
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).
49func (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.
58func 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
73func (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.
108func 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.
123func (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.
133func (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.
156func 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