github.com/xushiwei/go@v0.0.0-20130601165731-2b9d83f45bc9/src/cmd/godoc/template.go (about)

     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 main
    33  
    34  import (
    35  	"bytes"
    36  	"fmt"
    37  	"log"
    38  	"regexp"
    39  	"strings"
    40  	"text/template"
    41  )
    42  
    43  // Functions in this file panic on error, but the panic is recovered
    44  // to an error by 'code'.
    45  
    46  var templateFuncs = template.FuncMap{
    47  	"code": code,
    48  }
    49  
    50  // contents reads and returns the content of the named file
    51  // (from the virtual file system, so for example /doc refers to $GOROOT/doc).
    52  func contents(name string) string {
    53  	file, err := ReadFile(fs, name)
    54  	if err != nil {
    55  		log.Panic(err)
    56  	}
    57  	return string(file)
    58  }
    59  
    60  // stringFor returns a textual representation of the arg, formatted according to its nature.
    61  func stringFor(arg interface{}) string {
    62  	switch arg := arg.(type) {
    63  	case int:
    64  		return fmt.Sprintf("%d", arg)
    65  	case string:
    66  		if len(arg) > 2 && arg[0] == '/' && arg[len(arg)-1] == '/' {
    67  			return fmt.Sprintf("%#q", arg)
    68  		}
    69  		return fmt.Sprintf("%q", arg)
    70  	default:
    71  		log.Panicf("unrecognized argument: %v type %T", arg, arg)
    72  	}
    73  	return ""
    74  }
    75  
    76  func code(file string, arg ...interface{}) (s string, err error) {
    77  	defer func() {
    78  		if r := recover(); r != nil {
    79  			err = fmt.Errorf("%v", r)
    80  		}
    81  	}()
    82  
    83  	text := contents(file)
    84  	var command string
    85  	switch len(arg) {
    86  	case 0:
    87  		// text is already whole file.
    88  		command = fmt.Sprintf("code %q", file)
    89  	case 1:
    90  		command = fmt.Sprintf("code %q %s", file, stringFor(arg[0]))
    91  		text = oneLine(file, text, arg[0])
    92  	case 2:
    93  		command = fmt.Sprintf("code %q %s %s", file, stringFor(arg[0]), stringFor(arg[1]))
    94  		text = multipleLines(file, text, arg[0], arg[1])
    95  	default:
    96  		return "", fmt.Errorf("incorrect code invocation: code %q %q", file, arg)
    97  	}
    98  	// Trim spaces from output.
    99  	text = strings.Trim(text, "\n")
   100  	// Replace tabs by spaces, which work better in HTML.
   101  	text = strings.Replace(text, "\t", "    ", -1)
   102  	var buf bytes.Buffer
   103  	// HTML-escape text and syntax-color comments like elsewhere.
   104  	FormatText(&buf, []byte(text), -1, true, "", nil)
   105  	// Include the command as a comment.
   106  	text = fmt.Sprintf("<pre><!--{{%s}}\n-->%s</pre>", command, buf.Bytes())
   107  	return text, nil
   108  }
   109  
   110  // parseArg returns the integer or string value of the argument and tells which it is.
   111  func parseArg(arg interface{}, file string, max int) (ival int, sval string, isInt bool) {
   112  	switch n := arg.(type) {
   113  	case int:
   114  		if n <= 0 || n > max {
   115  			log.Panicf("%q:%d is out of range", file, n)
   116  		}
   117  		return n, "", true
   118  	case string:
   119  		return 0, n, false
   120  	}
   121  	log.Panicf("unrecognized argument %v type %T", arg, arg)
   122  	return
   123  }
   124  
   125  // oneLine returns the single line generated by a two-argument code invocation.
   126  func oneLine(file, text string, arg interface{}) string {
   127  	lines := strings.SplitAfter(contents(file), "\n")
   128  	line, pattern, isInt := parseArg(arg, file, len(lines))
   129  	if isInt {
   130  		return lines[line-1]
   131  	}
   132  	return lines[match(file, 0, lines, pattern)-1]
   133  }
   134  
   135  // multipleLines returns the text generated by a three-argument code invocation.
   136  func multipleLines(file, text string, arg1, arg2 interface{}) string {
   137  	lines := strings.SplitAfter(contents(file), "\n")
   138  	line1, pattern1, isInt1 := parseArg(arg1, file, len(lines))
   139  	line2, pattern2, isInt2 := parseArg(arg2, file, len(lines))
   140  	if !isInt1 {
   141  		line1 = match(file, 0, lines, pattern1)
   142  	}
   143  	if !isInt2 {
   144  		line2 = match(file, line1, lines, pattern2)
   145  	} else if line2 < line1 {
   146  		log.Panicf("lines out of order for %q: %d %d", text, line1, line2)
   147  	}
   148  	for k := line1 - 1; k < line2; k++ {
   149  		if strings.HasSuffix(lines[k], "OMIT\n") {
   150  			lines[k] = ""
   151  		}
   152  	}
   153  	return strings.Join(lines[line1-1:line2], "")
   154  }
   155  
   156  // match identifies the input line that matches the pattern in a code invocation.
   157  // If start>0, match lines starting there rather than at the beginning.
   158  // The return value is 1-indexed.
   159  func match(file string, start int, lines []string, pattern string) int {
   160  	// $ matches the end of the file.
   161  	if pattern == "$" {
   162  		if len(lines) == 0 {
   163  			log.Panicf("%q: empty file", file)
   164  		}
   165  		return len(lines)
   166  	}
   167  	// /regexp/ matches the line that matches the regexp.
   168  	if len(pattern) > 2 && pattern[0] == '/' && pattern[len(pattern)-1] == '/' {
   169  		re, err := regexp.Compile(pattern[1 : len(pattern)-1])
   170  		if err != nil {
   171  			log.Panic(err)
   172  		}
   173  		for i := start; i < len(lines); i++ {
   174  			if re.MatchString(lines[i]) {
   175  				return i + 1
   176  			}
   177  		}
   178  		log.Panicf("%s: no match for %#q", file, pattern)
   179  	}
   180  	log.Panicf("unrecognized pattern: %q", pattern)
   181  	return 0
   182  }