// Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // The template uses the function "code" to inject program // source into the output by extracting code from files and // injecting them as HTML-escaped
blocks. // // The syntax is simple: 1, 2, or 3 space-separated arguments: // // Whole file: // {{code "foo.go"}} // One line (here the signature of main): // {{code "foo.go" `/^func.main/`}} // Block of text, determined by start and end (here the body of main): // {{code "foo.go" `/^func.main/` `/^}/` // // Patterns can be `/regular expression/`, a decimal number, or "$" // to signify the end of the file. package main import ( "exp/template" "flag" "fmt" "io/ioutil" "log" "os" "regexp" "strings" ) func Usage() { fmt.Fprintf(os.Stderr, "usage: tmpltohtml file\n") os.Exit(2) } func main() { flag.Usage = Usage flag.Parse() if len(flag.Args()) != 1 { Usage() } // Read and parse the input. name := flag.Args()[0] tmpl := template.New(name).Funcs(template.FuncMap{"code": code}) if err := tmpl.ParseFile(name); err != nil { log.Fatal(err) } // Execute the template. if err := tmpl.Execute(os.Stdout, 0); err != nil { log.Fatal(err) } } // contents reads a file by name and returns its contents as a string. func contents(name string) string { file, err := ioutil.ReadFile(name) if err != nil { log.Fatal(err) } return string(file) } // format returns a textual representation of the arg, formatted according to its nature. func format(arg interface{}) string { switch arg := arg.(type) { case int: return fmt.Sprintf("%d", arg) case string: if len(arg) > 2 && arg[0] == '/' && arg[len(arg)-1] == '/' { return fmt.Sprintf("%#q", arg) } return fmt.Sprintf("%q", arg) default: log.Fatalf("unrecognized argument: %v type %T", arg, arg) } return "" } func code(file string, arg ...interface{}) (string, os.Error) { text := contents(file) var command string switch len(arg) { case 0: // text is already whole file. command = fmt.Sprintf("code %q", file) case 1: command = fmt.Sprintf("code %q %s", file, format(arg[0])) text = oneLine(file, text, arg[0]) case 2: command = fmt.Sprintf("code %q %s %s", file, format(arg[0]), format(arg[1])) text = multipleLines(file, text, arg[0], arg[1]) default: return "", fmt.Errorf("incorrect code invocation: code %q %q", file, arg) } // Replace tabs by spaces, which work better in HTML. text = strings.Replace(text, "\t", " ", -1) // Escape the program text for HTML. text = template.HTMLEscapeString(text) // Include the command as a comment. text = fmt.Sprintf("%s", command, text) return text, nil } // parseArg returns the integer or string value of the argument and tells which it is. func parseArg(arg interface{}, file string, max int) (ival int, sval string, isInt bool) { switch n := arg.(type) { case int: if n <= 0 || n > max { log.Fatalf("%q:%d is out of range", file, n) } return n, "", true case string: return 0, n, false } log.Fatalf("unrecognized argument %v type %T", arg, arg) return } // oneLine returns the single line generated by a two-argument code invocation. func oneLine(file, text string, arg interface{}) string { lines := strings.SplitAfter(contents(file), "\n") line, pattern, isInt := parseArg(arg, file, len(lines)) if isInt { return lines[line-1] } return lines[match(file, 0, lines, pattern)-1] } // multipleLines returns the text generated by a three-argument code invocation. func multipleLines(file, text string, arg1, arg2 interface{}) string { lines := strings.SplitAfter(contents(file), "\n") line1, pattern1, isInt1 := parseArg(arg1, file, len(lines)) line2, pattern2, isInt2 := parseArg(arg2, file, len(lines)) if !isInt1 { line1 = match(file, 0, lines, pattern1) } if !isInt2 { line2 = match(file, line1, lines, pattern2) } else if line2 < line1 { log.Fatal("lines out of order for %q: %d %d", line1, line2) } return strings.Join(lines[line1-1:line2], "") } // match identifies the input line that matches the pattern in a code invocation. // If start>0, match lines starting there rather than at the beginning. // The return value is 1-indexed. func match(file string, start int, lines []string, pattern string) int { // $ matches the end of the file. if pattern == "$" { if len(lines) == 0 { log.Fatal("%q: empty file", file) } return len(lines) } // /regexp/ matches the line that matches the regexp. if len(pattern) > 2 && pattern[0] == '/' && pattern[len(pattern)-1] == '/' { re, err := regexp.Compile(pattern[1 : len(pattern)-1]) if err != nil { log.Fatal(err) } for i := start; i < len(lines); i++ { if re.MatchString(lines[i]) { return i + 1 } } log.Fatalf("%s: no match for %#q", file, pattern) } log.Fatalf("unrecognized pattern: %q", pattern) return 0 }