mirror of
https://github.com/autc04/Retro68.git
synced 2024-11-28 05:51:04 +00:00
743 lines
18 KiB
Go
743 lines
18 KiB
Go
// Copyright 2009 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.
|
|
|
|
// Code to parse a template.
|
|
|
|
package template
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// Errors returned during parsing and execution. Users may extract the information and reformat
|
|
// if they desire.
|
|
type Error struct {
|
|
Line int
|
|
Msg string
|
|
}
|
|
|
|
func (e *Error) Error() string { return fmt.Sprintf("line %d: %s", e.Line, e.Msg) }
|
|
|
|
// checkError is a deferred function to turn a panic with type *Error into a plain error return.
|
|
// Other panics are unexpected and so are re-enabled.
|
|
func checkError(error *error) {
|
|
if v := recover(); v != nil {
|
|
if e, ok := v.(*Error); ok {
|
|
*error = e
|
|
} else {
|
|
// runtime errors should crash
|
|
panic(v)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Most of the literals are aces.
|
|
var lbrace = []byte{'{'}
|
|
var rbrace = []byte{'}'}
|
|
var space = []byte{' '}
|
|
var tab = []byte{'\t'}
|
|
|
|
// The various types of "tokens", which are plain text or (usually) brace-delimited descriptors
|
|
const (
|
|
tokAlternates = iota
|
|
tokComment
|
|
tokEnd
|
|
tokLiteral
|
|
tokOr
|
|
tokRepeated
|
|
tokSection
|
|
tokText
|
|
tokVariable
|
|
)
|
|
|
|
// FormatterMap is the type describing the mapping from formatter
|
|
// names to the functions that implement them.
|
|
type FormatterMap map[string]func(io.Writer, string, ...interface{})
|
|
|
|
// Built-in formatters.
|
|
var builtins = FormatterMap{
|
|
"html": HTMLFormatter,
|
|
"str": StringFormatter,
|
|
"": StringFormatter,
|
|
}
|
|
|
|
// The parsed state of a template is a vector of xxxElement structs.
|
|
// Sections have line numbers so errors can be reported better during execution.
|
|
|
|
// Plain text.
|
|
type textElement struct {
|
|
text []byte
|
|
}
|
|
|
|
// A literal such as .meta-left or .meta-right
|
|
type literalElement struct {
|
|
text []byte
|
|
}
|
|
|
|
// A variable invocation to be evaluated
|
|
type variableElement struct {
|
|
linenum int
|
|
args []interface{} // The fields and literals in the invocation.
|
|
fmts []string // Names of formatters to apply. len(fmts) > 0
|
|
}
|
|
|
|
// A variableElement arg to be evaluated as a field name
|
|
type fieldName string
|
|
|
|
// A .section block, possibly with a .or
|
|
type sectionElement struct {
|
|
linenum int // of .section itself
|
|
field string // cursor field for this block
|
|
start int // first element
|
|
or int // first element of .or block
|
|
end int // one beyond last element
|
|
}
|
|
|
|
// A .repeated block, possibly with a .or and a .alternates
|
|
type repeatedElement struct {
|
|
sectionElement // It has the same structure...
|
|
altstart int // ... except for alternates
|
|
altend int
|
|
}
|
|
|
|
// Template is the type that represents a template definition.
|
|
// It is unchanged after parsing.
|
|
type Template struct {
|
|
fmap FormatterMap // formatters for variables
|
|
// Used during parsing:
|
|
ldelim, rdelim []byte // delimiters; default {}
|
|
buf []byte // input text to process
|
|
p int // position in buf
|
|
linenum int // position in input
|
|
// Parsed results:
|
|
elems []interface{}
|
|
}
|
|
|
|
// New creates a new template with the specified formatter map (which
|
|
// may be nil) to define auxiliary functions for formatting variables.
|
|
func New(fmap FormatterMap) *Template {
|
|
t := new(Template)
|
|
t.fmap = fmap
|
|
t.ldelim = lbrace
|
|
t.rdelim = rbrace
|
|
t.elems = make([]interface{}, 0, 16)
|
|
return t
|
|
}
|
|
|
|
// Report error and stop executing. The line number must be provided explicitly.
|
|
func (t *Template) execError(st *state, line int, err string, args ...interface{}) {
|
|
panic(&Error{line, fmt.Sprintf(err, args...)})
|
|
}
|
|
|
|
// Report error, panic to terminate parsing.
|
|
// The line number comes from the template state.
|
|
func (t *Template) parseError(err string, args ...interface{}) {
|
|
panic(&Error{t.linenum, fmt.Sprintf(err, args...)})
|
|
}
|
|
|
|
// Is this an exported - upper case - name?
|
|
func isExported(name string) bool {
|
|
r, _ := utf8.DecodeRuneInString(name)
|
|
return unicode.IsUpper(r)
|
|
}
|
|
|
|
// -- Lexical analysis
|
|
|
|
// Is c a space character?
|
|
func isSpace(c uint8) bool { return c == ' ' || c == '\t' || c == '\r' || c == '\n' }
|
|
|
|
// Safely, does s[n:n+len(t)] == t?
|
|
func equal(s []byte, n int, t []byte) bool {
|
|
b := s[n:]
|
|
if len(t) > len(b) { // not enough space left for a match.
|
|
return false
|
|
}
|
|
for i, c := range t {
|
|
if c != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// isQuote returns true if c is a string- or character-delimiting quote character.
|
|
func isQuote(c byte) bool {
|
|
return c == '"' || c == '`' || c == '\''
|
|
}
|
|
|
|
// endQuote returns the end quote index for the quoted string that
|
|
// starts at n, or -1 if no matching end quote is found before the end
|
|
// of the line.
|
|
func endQuote(s []byte, n int) int {
|
|
quote := s[n]
|
|
for n++; n < len(s); n++ {
|
|
switch s[n] {
|
|
case '\\':
|
|
if quote == '"' || quote == '\'' {
|
|
n++
|
|
}
|
|
case '\n':
|
|
return -1
|
|
case quote:
|
|
return n
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// nextItem returns the next item from the input buffer. If the returned
|
|
// item is empty, we are at EOF. The item will be either a
|
|
// delimited string or a non-empty string between delimited
|
|
// strings. Tokens stop at (but include, if plain text) a newline.
|
|
// Action tokens on a line by themselves drop any space on
|
|
// either side, up to and including the newline.
|
|
func (t *Template) nextItem() []byte {
|
|
startOfLine := t.p == 0 || t.buf[t.p-1] == '\n'
|
|
start := t.p
|
|
var i int
|
|
newline := func() {
|
|
t.linenum++
|
|
i++
|
|
}
|
|
// Leading space up to but not including newline
|
|
for i = start; i < len(t.buf); i++ {
|
|
if t.buf[i] == '\n' || !isSpace(t.buf[i]) {
|
|
break
|
|
}
|
|
}
|
|
leadingSpace := i > start
|
|
// What's left is nothing, newline, delimited string, or plain text
|
|
switch {
|
|
case i == len(t.buf):
|
|
// EOF; nothing to do
|
|
case t.buf[i] == '\n':
|
|
newline()
|
|
case equal(t.buf, i, t.ldelim):
|
|
left := i // Start of left delimiter.
|
|
right := -1 // Will be (immediately after) right delimiter.
|
|
haveText := false // Delimiters contain text.
|
|
i += len(t.ldelim)
|
|
// Find the end of the action.
|
|
for ; i < len(t.buf); i++ {
|
|
if t.buf[i] == '\n' {
|
|
break
|
|
}
|
|
if isQuote(t.buf[i]) {
|
|
i = endQuote(t.buf, i)
|
|
if i == -1 {
|
|
t.parseError("unmatched quote")
|
|
return nil
|
|
}
|
|
continue
|
|
}
|
|
if equal(t.buf, i, t.rdelim) {
|
|
i += len(t.rdelim)
|
|
right = i
|
|
break
|
|
}
|
|
haveText = true
|
|
}
|
|
if right < 0 {
|
|
t.parseError("unmatched opening delimiter")
|
|
return nil
|
|
}
|
|
// Is this a special action (starts with '.' or '#') and the only thing on the line?
|
|
if startOfLine && haveText {
|
|
firstChar := t.buf[left+len(t.ldelim)]
|
|
if firstChar == '.' || firstChar == '#' {
|
|
// It's special and the first thing on the line. Is it the last?
|
|
for j := right; j < len(t.buf) && isSpace(t.buf[j]); j++ {
|
|
if t.buf[j] == '\n' {
|
|
// Yes it is. Drop the surrounding space and return the {.foo}
|
|
t.linenum++
|
|
t.p = j + 1
|
|
return t.buf[left:right]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// No it's not. If there's leading space, return that.
|
|
if leadingSpace {
|
|
// not trimming space: return leading space if there is some.
|
|
t.p = left
|
|
return t.buf[start:left]
|
|
}
|
|
// Return the word, leave the trailing space.
|
|
start = left
|
|
break
|
|
default:
|
|
for ; i < len(t.buf); i++ {
|
|
if t.buf[i] == '\n' {
|
|
newline()
|
|
break
|
|
}
|
|
if equal(t.buf, i, t.ldelim) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
item := t.buf[start:i]
|
|
t.p = i
|
|
return item
|
|
}
|
|
|
|
// Turn a byte array into a space-split array of strings,
|
|
// taking into account quoted strings.
|
|
func words(buf []byte) []string {
|
|
s := make([]string, 0, 5)
|
|
for i := 0; i < len(buf); {
|
|
// One word per loop
|
|
for i < len(buf) && isSpace(buf[i]) {
|
|
i++
|
|
}
|
|
if i == len(buf) {
|
|
break
|
|
}
|
|
// Got a word
|
|
start := i
|
|
if isQuote(buf[i]) {
|
|
i = endQuote(buf, i)
|
|
if i < 0 {
|
|
i = len(buf)
|
|
} else {
|
|
i++
|
|
}
|
|
}
|
|
// Even with quotes, break on space only. This handles input
|
|
// such as {""|} and catches quoting mistakes.
|
|
for i < len(buf) && !isSpace(buf[i]) {
|
|
i++
|
|
}
|
|
s = append(s, string(buf[start:i]))
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Analyze an item and return its token type and, if it's an action item, an array of
|
|
// its constituent words.
|
|
func (t *Template) analyze(item []byte) (tok int, w []string) {
|
|
// item is known to be non-empty
|
|
if !equal(item, 0, t.ldelim) { // doesn't start with left delimiter
|
|
tok = tokText
|
|
return
|
|
}
|
|
if !equal(item, len(item)-len(t.rdelim), t.rdelim) { // doesn't end with right delimiter
|
|
t.parseError("internal error: unmatched opening delimiter") // lexing should prevent this
|
|
return
|
|
}
|
|
if len(item) <= len(t.ldelim)+len(t.rdelim) { // no contents
|
|
t.parseError("empty directive")
|
|
return
|
|
}
|
|
// Comment
|
|
if item[len(t.ldelim)] == '#' {
|
|
tok = tokComment
|
|
return
|
|
}
|
|
// Split into words
|
|
w = words(item[len(t.ldelim) : len(item)-len(t.rdelim)]) // drop final delimiter
|
|
if len(w) == 0 {
|
|
t.parseError("empty directive")
|
|
return
|
|
}
|
|
first := w[0]
|
|
if first[0] != '.' {
|
|
tok = tokVariable
|
|
return
|
|
}
|
|
if len(first) > 1 && first[1] >= '0' && first[1] <= '9' {
|
|
// Must be a float.
|
|
tok = tokVariable
|
|
return
|
|
}
|
|
switch first {
|
|
case ".meta-left", ".meta-right", ".space", ".tab":
|
|
tok = tokLiteral
|
|
return
|
|
case ".or":
|
|
tok = tokOr
|
|
return
|
|
case ".end":
|
|
tok = tokEnd
|
|
return
|
|
case ".section":
|
|
if len(w) != 2 {
|
|
t.parseError("incorrect fields for .section: %s", item)
|
|
return
|
|
}
|
|
tok = tokSection
|
|
return
|
|
case ".repeated":
|
|
if len(w) != 3 || w[1] != "section" {
|
|
t.parseError("incorrect fields for .repeated: %s", item)
|
|
return
|
|
}
|
|
tok = tokRepeated
|
|
return
|
|
case ".alternates":
|
|
if len(w) != 2 || w[1] != "with" {
|
|
t.parseError("incorrect fields for .alternates: %s", item)
|
|
return
|
|
}
|
|
tok = tokAlternates
|
|
return
|
|
}
|
|
t.parseError("bad directive: %s", item)
|
|
return
|
|
}
|
|
|
|
// formatter returns the Formatter with the given name in the Template, or nil if none exists.
|
|
func (t *Template) formatter(name string) func(io.Writer, string, ...interface{}) {
|
|
if t.fmap != nil {
|
|
if fn := t.fmap[name]; fn != nil {
|
|
return fn
|
|
}
|
|
}
|
|
return builtins[name]
|
|
}
|
|
|
|
// -- Parsing
|
|
|
|
// newVariable allocates a new variable-evaluation element.
|
|
func (t *Template) newVariable(words []string) *variableElement {
|
|
formatters := extractFormatters(words)
|
|
args := make([]interface{}, len(words))
|
|
|
|
// Build argument list, processing any literals
|
|
for i, word := range words {
|
|
var lerr error
|
|
switch word[0] {
|
|
case '"', '`', '\'':
|
|
v, err := strconv.Unquote(word)
|
|
if err == nil && word[0] == '\'' {
|
|
args[i], _ = utf8.DecodeRuneInString(v)
|
|
} else {
|
|
args[i], lerr = v, err
|
|
}
|
|
|
|
case '.', '+', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
|
v, err := strconv.ParseInt(word, 0, 64)
|
|
if err == nil {
|
|
args[i] = v
|
|
} else {
|
|
v, err := strconv.ParseFloat(word, 64)
|
|
args[i], lerr = v, err
|
|
}
|
|
|
|
default:
|
|
args[i] = fieldName(word)
|
|
}
|
|
if lerr != nil {
|
|
t.parseError("invalid literal: %q: %s", word, lerr)
|
|
}
|
|
}
|
|
|
|
// We could remember the function address here and avoid the lookup later,
|
|
// but it's more dynamic to let the user change the map contents underfoot.
|
|
// We do require the name to be present, though.
|
|
|
|
// Is it in user-supplied map?
|
|
for _, f := range formatters {
|
|
if t.formatter(f) == nil {
|
|
t.parseError("unknown formatter: %q", f)
|
|
}
|
|
}
|
|
|
|
return &variableElement{t.linenum, args, formatters}
|
|
}
|
|
|
|
// extractFormatters extracts a list of formatters from words.
|
|
// After the final space-separated argument in a variable, formatters may be
|
|
// specified separated by pipe symbols. For example: {a b c|d|e}
|
|
// The words parameter still has the formatters joined by '|' in the last word.
|
|
// extractFormatters splits formatters, replaces the last word with the content
|
|
// found before the first '|' within it, and returns the formatters obtained.
|
|
// If no formatters are found in words, the default formatter is returned.
|
|
func extractFormatters(words []string) (formatters []string) {
|
|
// "" is the default formatter.
|
|
formatters = []string{""}
|
|
if len(words) == 0 {
|
|
return
|
|
}
|
|
var bar int
|
|
lastWord := words[len(words)-1]
|
|
if isQuote(lastWord[0]) {
|
|
end := endQuote([]byte(lastWord), 0)
|
|
if end < 0 || end+1 == len(lastWord) || lastWord[end+1] != '|' {
|
|
return
|
|
}
|
|
bar = end + 1
|
|
} else {
|
|
bar = strings.IndexRune(lastWord, '|')
|
|
if bar < 0 {
|
|
return
|
|
}
|
|
}
|
|
words[len(words)-1] = lastWord[0:bar]
|
|
formatters = strings.Split(lastWord[bar+1:], "|")
|
|
return
|
|
}
|
|
|
|
// Grab the next item. If it's simple, just append it to the template.
|
|
// Otherwise return its details.
|
|
func (t *Template) parseSimple(item []byte) (done bool, tok int, w []string) {
|
|
tok, w = t.analyze(item)
|
|
done = true // assume for simplicity
|
|
switch tok {
|
|
case tokComment:
|
|
return
|
|
case tokText:
|
|
t.elems = append(t.elems, &textElement{item})
|
|
return
|
|
case tokLiteral:
|
|
switch w[0] {
|
|
case ".meta-left":
|
|
t.elems = append(t.elems, &literalElement{t.ldelim})
|
|
case ".meta-right":
|
|
t.elems = append(t.elems, &literalElement{t.rdelim})
|
|
case ".space":
|
|
t.elems = append(t.elems, &literalElement{space})
|
|
case ".tab":
|
|
t.elems = append(t.elems, &literalElement{tab})
|
|
default:
|
|
t.parseError("internal error: unknown literal: %s", w[0])
|
|
}
|
|
return
|
|
case tokVariable:
|
|
t.elems = append(t.elems, t.newVariable(w))
|
|
return
|
|
}
|
|
return false, tok, w
|
|
}
|
|
|
|
// parseRepeated and parseSection are mutually recursive
|
|
|
|
func (t *Template) parseRepeated(words []string) *repeatedElement {
|
|
r := new(repeatedElement)
|
|
t.elems = append(t.elems, r)
|
|
r.linenum = t.linenum
|
|
r.field = words[2]
|
|
// Scan section, collecting true and false (.or) blocks.
|
|
r.start = len(t.elems)
|
|
r.or = -1
|
|
r.altstart = -1
|
|
r.altend = -1
|
|
Loop:
|
|
for {
|
|
item := t.nextItem()
|
|
if len(item) == 0 {
|
|
t.parseError("missing .end for .repeated section")
|
|
break
|
|
}
|
|
done, tok, w := t.parseSimple(item)
|
|
if done {
|
|
continue
|
|
}
|
|
switch tok {
|
|
case tokEnd:
|
|
break Loop
|
|
case tokOr:
|
|
if r.or >= 0 {
|
|
t.parseError("extra .or in .repeated section")
|
|
break Loop
|
|
}
|
|
r.altend = len(t.elems)
|
|
r.or = len(t.elems)
|
|
case tokSection:
|
|
t.parseSection(w)
|
|
case tokRepeated:
|
|
t.parseRepeated(w)
|
|
case tokAlternates:
|
|
if r.altstart >= 0 {
|
|
t.parseError("extra .alternates in .repeated section")
|
|
break Loop
|
|
}
|
|
if r.or >= 0 {
|
|
t.parseError(".alternates inside .or block in .repeated section")
|
|
break Loop
|
|
}
|
|
r.altstart = len(t.elems)
|
|
default:
|
|
t.parseError("internal error: unknown repeated section item: %s", item)
|
|
break Loop
|
|
}
|
|
}
|
|
if r.altend < 0 {
|
|
r.altend = len(t.elems)
|
|
}
|
|
r.end = len(t.elems)
|
|
return r
|
|
}
|
|
|
|
func (t *Template) parseSection(words []string) *sectionElement {
|
|
s := new(sectionElement)
|
|
t.elems = append(t.elems, s)
|
|
s.linenum = t.linenum
|
|
s.field = words[1]
|
|
// Scan section, collecting true and false (.or) blocks.
|
|
s.start = len(t.elems)
|
|
s.or = -1
|
|
Loop:
|
|
for {
|
|
item := t.nextItem()
|
|
if len(item) == 0 {
|
|
t.parseError("missing .end for .section")
|
|
break
|
|
}
|
|
done, tok, w := t.parseSimple(item)
|
|
if done {
|
|
continue
|
|
}
|
|
switch tok {
|
|
case tokEnd:
|
|
break Loop
|
|
case tokOr:
|
|
if s.or >= 0 {
|
|
t.parseError("extra .or in .section")
|
|
break Loop
|
|
}
|
|
s.or = len(t.elems)
|
|
case tokSection:
|
|
t.parseSection(w)
|
|
case tokRepeated:
|
|
t.parseRepeated(w)
|
|
case tokAlternates:
|
|
t.parseError(".alternates not in .repeated")
|
|
default:
|
|
t.parseError("internal error: unknown section item: %s", item)
|
|
}
|
|
}
|
|
s.end = len(t.elems)
|
|
return s
|
|
}
|
|
|
|
func (t *Template) parse() {
|
|
for {
|
|
item := t.nextItem()
|
|
if len(item) == 0 {
|
|
break
|
|
}
|
|
done, tok, w := t.parseSimple(item)
|
|
if done {
|
|
continue
|
|
}
|
|
switch tok {
|
|
case tokOr, tokEnd, tokAlternates:
|
|
t.parseError("unexpected %s", w[0])
|
|
case tokSection:
|
|
t.parseSection(w)
|
|
case tokRepeated:
|
|
t.parseRepeated(w)
|
|
default:
|
|
t.parseError("internal error: bad directive in parse: %s", item)
|
|
}
|
|
}
|
|
}
|
|
|
|
// -- Execution
|
|
|
|
// -- Public interface
|
|
|
|
// Parse initializes a Template by parsing its definition. The string
|
|
// s contains the template text. If any errors occur, Parse returns
|
|
// the error.
|
|
func (t *Template) Parse(s string) (err error) {
|
|
if t.elems == nil {
|
|
return &Error{1, "template not allocated with New"}
|
|
}
|
|
if !validDelim(t.ldelim) || !validDelim(t.rdelim) {
|
|
return &Error{1, fmt.Sprintf("bad delimiter strings %q %q", t.ldelim, t.rdelim)}
|
|
}
|
|
defer checkError(&err)
|
|
t.buf = []byte(s)
|
|
t.p = 0
|
|
t.linenum = 1
|
|
t.parse()
|
|
return nil
|
|
}
|
|
|
|
// ParseFile is like Parse but reads the template definition from the
|
|
// named file.
|
|
func (t *Template) ParseFile(filename string) (err error) {
|
|
b, err := ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return t.Parse(string(b))
|
|
}
|
|
|
|
// Execute applies a parsed template to the specified data object,
|
|
// generating output to wr.
|
|
func (t *Template) Execute(wr io.Writer, data interface{}) (err error) {
|
|
// Extract the driver data.
|
|
val := reflect.ValueOf(data)
|
|
defer checkError(&err)
|
|
t.p = 0
|
|
t.execute(0, len(t.elems), &state{parent: nil, data: val, wr: wr})
|
|
return nil
|
|
}
|
|
|
|
// SetDelims sets the left and right delimiters for operations in the
|
|
// template. They are validated during parsing. They could be
|
|
// validated here but it's better to keep the routine simple. The
|
|
// delimiters are very rarely invalid and Parse has the necessary
|
|
// error-handling interface already.
|
|
func (t *Template) SetDelims(left, right string) {
|
|
t.ldelim = []byte(left)
|
|
t.rdelim = []byte(right)
|
|
}
|
|
|
|
// Parse creates a Template with default parameters (such as {} for
|
|
// metacharacters). The string s contains the template text while
|
|
// the formatter map fmap, which may be nil, defines auxiliary functions
|
|
// for formatting variables. The template is returned. If any errors
|
|
// occur, err will be non-nil.
|
|
func Parse(s string, fmap FormatterMap) (t *Template, err error) {
|
|
t = New(fmap)
|
|
err = t.Parse(s)
|
|
if err != nil {
|
|
t = nil
|
|
}
|
|
return
|
|
}
|
|
|
|
// ParseFile is a wrapper function that creates a Template with default
|
|
// parameters (such as {} for metacharacters). The filename identifies
|
|
// a file containing the template text, while the formatter map fmap, which
|
|
// may be nil, defines auxiliary functions for formatting variables.
|
|
// The template is returned. If any errors occur, err will be non-nil.
|
|
func ParseFile(filename string, fmap FormatterMap) (t *Template, err error) {
|
|
b, err := ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return Parse(string(b), fmap)
|
|
}
|
|
|
|
// MustParse is like Parse but panics if the template cannot be parsed.
|
|
func MustParse(s string, fmap FormatterMap) *Template {
|
|
t, err := Parse(s, fmap)
|
|
if err != nil {
|
|
panic("template.MustParse error: " + err.Error())
|
|
}
|
|
return t
|
|
}
|
|
|
|
// MustParseFile is like ParseFile but panics if the file cannot be read
|
|
// or the template cannot be parsed.
|
|
func MustParseFile(filename string, fmap FormatterMap) *Template {
|
|
b, err := ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
panic("template.MustParseFile error: " + err.Error())
|
|
}
|
|
return MustParse(string(b), fmap)
|
|
}
|