github.com/v2fly/tools@v0.100.0/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 godoc
    33  
    34  import (
    35  	"bytes"
    36  	"fmt"
    37  	"log"
    38  	"regexp"
    39  	"strings"
    40  
    41  	"github.com/v2fly/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  }