Commits

Sean Russell committed b13e283

Fixed for go.1

  • Participants

Comments (0)

Files changed (12)

+include $(GOROOT)/src/Make.inc
+
+all: simple
+
+clean:
+	rm -f simple
+
+%.${O}: %.go
+	${GC} -I . $<
+
+%: %.${O} ../src/fastweb/_obj/go-fastweb.googlecode.com/svn/trunk/src/fastweb.a
+	${LD} -L ../src/fastweb/_obj:. -o $@ $<
+

example/htdocs/css/default.css

+body {
+	font-family: Arial, Helvetica, sans-serif;
+	font-size: 0.8em;
+	margin: 0;
+	min-width: 600px;
+}
+
+div#mainbody {
+	padding: 0.5em 1.5em 1.5em;
+}
+
+div#navigation div {
+	border-right: 1px solid #5599dd;
+	float: left;
+	position: relative;
+}
+
+div#navigation {
+	background-color: #6daae7;
+	float: left;
+	width: 100%;
+}
+
+div#navigation div a, .topTab {
+	color: #ffffff;
+	cursor: pointer;
+	display: block;
+	font-weight: bold;
+	padding: 5px 15px;
+	text-decoration: none;
+}
+
+a {
+	outline-color: -moz-use-text-color;
+	outline-style: none;
+	outline-width: medium;
+}
+
+#footer {
+	border-top: 3px solid #5599dd;
+	color: #999999;
+	margin: 2em 0 0;
+	padding: 2px 0 0;
+	text-align: center;
+}
+
+div#content {
+	margin: 20px 30px;
+}
+
+.clear {
+	clear:both;
+}
+

example/htdocs/img/ah64.jpg

Added
New image

example/htdocs/img/logo.png

Added
New image

example/simple.go

+// vim: set syntax=go autoindent:
+// Copyright 2010 Ivan Wong. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+package main
+
+import (
+        "go-fastweb.googlecode.com/svn/trunk/src/fastweb"
+        "os"
+)
+
+type Products struct {
+	fastweb.Controller
+	Name string
+	Brand string
+	Features []string
+	Specifications []string
+	Image string
+}
+
+func (p *Products) View(id string) os.Error {
+	if id == "ah64" {
+		p.Name = "RC Apache AH64 4-Channel Electric Helicoper"
+		p.Brand = "Colco"
+		p.Features = []string{
+			"4 channel radio control duel propeller system",
+			"Full movement controll: forward, backward, left, right, up and down",
+			"Replica design",
+			"Revolutionary co-axial rotor technology",
+		}
+		p.Specifications = []string{
+			"Dimensions: L 16 Inches X W 5.5 Inches x H 6.5 Inches",
+			"Battery Duration: 10 min",
+			"Range: 120 Feet",
+		}
+		p.Image = "/img/ah64.jpg"
+	}
+	return nil
+}
+
+func main() {
+	a := fastweb.NewApplication()
+	a.RegisterController(&Products{})
+        a.Run(":12345")
+}
+

example/views/layouts/default.tpl

+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> 
+<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr"> 
+<head> 
+<meta http-equiv="content-type" content="text/html; charset=UTF-8"> 
+<title><%PageTitle%></title> 
+<link rel="stylesheet" type="text/css" href="/css/default.css"> 
+</head> 
+<body> 
+<div id="mainbody"> 
+  <div id="header"> 
+    <div id="logo"><img src="/img/logo.png" alt="RC Toys" width="170" height="82"></div> 
+  </div> 
+  <div id="navigation"> 
+    <div><a href="/home" title="home">Home</a></div>
+    <div><a href="/products" title="search products">Products</a></div>
+    <div><a href="/cart" title="shopping cart">Shopping Cart</a></div>
+  </div> 
+  <div class="clear"></div> 
+  <div id="content"> 
+  <%RenderContent%>
+  </div> 
+  <div id="footer"> 
+    <p></p> 
+    <span>&copy; 2010 RC Toys.</span> 
+  </div> 
+</div> 
+</body> 
+</html> 
+<!-- vim: set syntax=php autoindent shiftwidth=2 tabstop=8 softtabstop=2 expandtab: --> 

example/views/products/view.tpl

+<%.section Name%>
+Name: <%Name%><br/>
+Manufacturer: <%Brand%><br/>
+<%.section Image%>
+<img src="<%Image%>"><br/>
+<%.end%>
+<%.section Features%>
+Features:<br/>
+<ul>
+<%.repeated section @%>
+<li><%@%></li>
+<%.end%>
+</ul>
+<%.end%>
+<%.section Specifications%>
+Specifications:<br/>
+<ul>
+<%.repeated section @%>
+<li><%@%></li>
+<%.end%>
+</ul>
+<%.end%>
+<%.or%>
+No product was found.
+<%.end%>
+// 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 execute a parsed template.
+
+package fastweb
+
+import (
+	"bytes"
+	"io"
+	"reflect"
+	"strings"
+)
+
+// Internal state for executing a Template.  As we evaluate the struct,
+// the data item descends into the fields associated with sections, etc.
+// Parent is used to walk upwards to find variables higher in the tree.
+type state struct {
+	parent *state          // parent in hierarchy
+	data   reflect.Value   // the driver data for this section etc.
+	wr     io.Writer       // where to send output
+	buf    [2]bytes.Buffer // alternating buffers used when chaining formatters
+}
+
+func (parent *state) clone(data reflect.Value) *state {
+	return &state{parent: parent, data: data, wr: parent.wr}
+}
+
+// Evaluate interfaces and pointers looking for a value that can look up the name, via a
+// struct field, method, or map key, and return the result of the lookup.
+func (t *Template) lookup(st *state, v reflect.Value, name string) reflect.Value {
+	for v.IsValid() {
+		typ := v.Type()
+		if n := v.Type().NumMethod(); n > 0 {
+			var params []reflect.Value
+			parts := strings.SplitN(name, ":", 2)
+			name = parts[0]
+			nIn := 1
+			if len(parts) > 1 {
+				params = []reflect.Value{reflect.ValueOf(parts[1])}
+				nIn++
+			}
+			for i := 0; i < n; i++ {
+				m := typ.Method(i)
+				mtyp := m.Type
+				if m.Name == name && mtyp.NumIn() == nIn && mtyp.NumOut() == 1 {
+					if !isExported(name) {
+						t.execError(st, t.linenum, "name not exported: %s in type %s", name, st.data.Type())
+					}
+					return v.Method(i).Call(params)[0]
+				}
+			}
+		}
+		switch av := v; av.Kind() {
+		case reflect.Ptr:
+			v = av.Elem()
+		case reflect.Interface:
+			v = av.Elem()
+		case reflect.Struct:
+			if !isExported(name) {
+				t.execError(st, t.linenum, "name not exported: %s in type %s", name, st.data.Type())
+			}
+			return av.FieldByName(name)
+		case reflect.Map:
+			if v := av.MapIndex(reflect.ValueOf(name)); v.IsValid() {
+				return v
+			}
+			return reflect.Zero(typ.Elem())
+		default:
+			return reflect.Value{}
+		}
+	}
+	return v
+}
+
+// indirectPtr returns the item numLevels levels of indirection below the value.
+// It is forgiving: if the value is not a pointer, it returns it rather than giving
+// an error.  If the pointer is nil, it is returned as is.
+func indirectPtr(v reflect.Value, numLevels int) reflect.Value {
+	for i := numLevels; v.IsValid() && i > 0; i++ {
+		if p := v; p.Kind() == reflect.Ptr {
+			if p.IsNil() {
+				return v
+			}
+			v = p.Elem()
+		} else {
+			break
+		}
+	}
+	return v
+}
+
+// Walk v through pointers and interfaces, extracting the elements within.
+func indirect(v reflect.Value) reflect.Value {
+loop:
+	for v.IsValid() {
+		switch av := v; av.Kind() {
+		case reflect.Ptr:
+			v = av.Elem()
+		case reflect.Interface:
+			v = av.Elem()
+		default:
+			break loop
+		}
+	}
+	return v
+}
+
+// If the data for this template is a struct, find the named variable.
+// Names of the form a.b.c are walked down the data tree.
+// The special name "@" (the "cursor") denotes the current data.
+// The value coming in (st.data) might need indirecting to reach
+// a struct while the return value is not indirected - that is,
+// it represents the actual named field. Leading stars indicate
+// levels of indirection to be applied to the value.
+func (t *Template) findVar(st *state, s string) reflect.Value {
+	data := st.data
+	flattenedName := strings.TrimLeft(s, "*")
+	numStars := len(s) - len(flattenedName)
+	s = flattenedName
+	if s == "@" {
+		return indirectPtr(data, numStars)
+	}
+	for _, elem := range strings.Split(s, ".") {
+		// Look up field; data must be a struct or map.
+		data = t.lookup(st, data, elem)
+		if !data.IsValid() {
+			return reflect.Value{}
+		}
+	}
+	return indirectPtr(data, numStars)
+}
+
+// Is there no data to look at?
+func empty(v reflect.Value) bool {
+	v = indirect(v)
+	if !v.IsValid() {
+		return true
+	}
+	switch v.Kind() {
+	case reflect.Bool:
+		return v.Bool() == false
+	case reflect.String:
+		return v.String() == ""
+	case reflect.Struct:
+		return false
+	case reflect.Map:
+		return false
+	case reflect.Array:
+		return v.Len() == 0
+	case reflect.Slice:
+		return v.Len() == 0
+	}
+	return false
+}
+
+// Look up a variable or method, up through the parent if necessary.
+func (t *Template) varValue(name string, st *state) reflect.Value {
+	field := t.findVar(st, name)
+	if !field.IsValid() {
+		if st.parent == nil {
+			t.execError(st, t.linenum, "name not found: %s in type %s", name, st.data.Type())
+		}
+		return t.varValue(name, st.parent)
+	}
+	return field
+}
+
+func (t *Template) format(wr io.Writer, fmt string, val []interface{}, v *variableElement, st *state) {
+	fn := t.formatter(fmt)
+	if fn == nil {
+		t.execError(st, v.linenum, "missing formatter %s for variable", fmt)
+	}
+	fn(wr, fmt, val...)
+}
+
+// Evaluate a variable, looking up through the parent if necessary.
+// If it has a formatter attached ({var|formatter}) run that too.
+func (t *Template) writeVariable(v *variableElement, st *state) {
+	// Resolve field names
+	val := make([]interface{}, len(v.args))
+	for i, arg := range v.args {
+		if name, ok := arg.(fieldName); ok {
+			val[i] = t.varValue(string(name), st).Interface()
+		} else {
+			val[i] = arg
+		}
+	}
+	for i, fmt := range v.fmts[:len(v.fmts)-1] {
+		b := &st.buf[i&1]
+		b.Reset()
+		t.format(b, fmt, val, v, st)
+		val = val[0:1]
+		val[0] = b.Bytes()
+	}
+	t.format(st.wr, v.fmts[len(v.fmts)-1], val, v, st)
+}
+
+// Execute element i.  Return next index to execute.
+func (t *Template) executeElement(i int, st *state) int {
+	switch elem := t.elems[i].(type) {
+	case *textElement:
+		st.wr.Write(elem.text)
+		return i + 1
+	case *literalElement:
+		st.wr.Write(elem.text)
+		return i + 1
+	case *variableElement:
+		t.writeVariable(elem, st)
+		return i + 1
+	case *sectionElement:
+		t.executeSection(elem, st)
+		return elem.end
+	case *repeatedElement:
+		t.executeRepeated(elem, st)
+		return elem.end
+	}
+	e := t.elems[i]
+	t.execError(st, 0, "internal error: bad directive in execute: %v %T\n", reflect.ValueOf(e).Interface(), e)
+	return 0
+}
+
+// Execute the template.
+func (t *Template) execute(start, end int, st *state) {
+	for i := start; i < end; {
+		i = t.executeElement(i, st)
+	}
+}
+
+// Execute a .section
+func (t *Template) executeSection(s *sectionElement, st *state) {
+	// Find driver data for this section.  It must be in the current struct.
+	field := t.varValue(s.field, st)
+	if !field.IsValid() {
+		t.execError(st, s.linenum, ".section: cannot find field %s in %s", s.field, st.data.Type())
+	}
+	st = st.clone(field)
+	start, end := s.start, s.or
+	if !empty(field) {
+		// Execute the normal block.
+		if end < 0 {
+			end = s.end
+		}
+	} else {
+		// Execute the .or block.  If it's missing, do nothing.
+		start, end = s.or, s.end
+		if start < 0 {
+			return
+		}
+	}
+	for i := start; i < end; {
+		i = t.executeElement(i, st)
+	}
+}
+
+// Return the result of calling the Iter method on v, or nil.
+func iter(v reflect.Value) reflect.Value {
+	for j := 0; j < v.Type().NumMethod(); j++ {
+		mth := v.Type().Method(j)
+		fv := v.Method(j)
+		ft := fv.Type()
+		// TODO(rsc): NumIn() should return 0 here, because ft is from a curried FuncValue.
+		if mth.Name != "Iter" || ft.NumIn() != 1 || ft.NumOut() != 1 {
+			continue
+		}
+		ct := ft.Out(0)
+		if ct.Kind() != reflect.Chan ||
+			ct.ChanDir()&reflect.RecvDir == 0 {
+			continue
+		}
+		return fv.Call(nil)[0]
+	}
+	return reflect.Value{}
+}
+
+// Execute a .repeated section
+func (t *Template) executeRepeated(r *repeatedElement, st *state) {
+	// Find driver data for this section.  It must be in the current struct.
+	field := t.varValue(r.field, st)
+	if !field.IsValid() {
+		t.execError(st, r.linenum, ".repeated: cannot find field %s in %s", r.field, st.data.Type())
+	}
+	field = indirect(field)
+
+	start, end := r.start, r.or
+	if end < 0 {
+		end = r.end
+	}
+	if r.altstart >= 0 {
+		end = r.altstart
+	}
+	first := true
+
+	// Code common to all the loops.
+	loopBody := func(newst *state) {
+		// .alternates between elements
+		if !first && r.altstart >= 0 {
+			for i := r.altstart; i < r.altend; {
+				i = t.executeElement(i, newst)
+			}
+		}
+		first = false
+		for i := start; i < end; {
+			i = t.executeElement(i, newst)
+		}
+	}
+
+	if array := field; array.Kind() == reflect.Array || array.Kind() == reflect.Slice {
+		for j := 0; j < array.Len(); j++ {
+			loopBody(st.clone(array.Index(j)))
+		}
+	} else if m := field; m.Kind() == reflect.Map {
+		for _, key := range m.MapKeys() {
+			loopBody(st.clone(m.MapIndex(key)))
+		}
+	} else if ch := iter(field); ch.IsValid() {
+		for {
+			e, ok := ch.Recv()
+			if !ok {
+				break
+			}
+			loopBody(st.clone(e))
+		}
+	} else {
+		t.execError(st, r.linenum, ".repeated: cannot repeat %s (type %s)",
+			r.field, field.Type())
+	}
+
+	if first {
+		// Empty. Execute the .or block, once.  If it's missing, do nothing.
+		start, end := r.or, r.end
+		if start >= 0 {
+			newst := st.clone(field)
+			for i := start; i < end; {
+				i = t.executeElement(i, newst)
+			}
+		}
+		return
+	}
+}
+
+// A valid delimiter must contain no space and be non-empty.
+func validDelim(d []byte) bool {
+	if len(d) == 0 {
+		return false
+	}
+	for _, c := range d {
+		if isSpace(c) {
+			return false
+		}
+	}
+	return true
+}
+// vim: set syntax=go autoindent:
+// Copyright 2010 Ivan Wong. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+package fastweb
+
+import (
+	"bufio"
+	"bytes"
+	"code.google.com/p/go-fastcgi"
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"math/rand"
+	"net/url"
+	"os"
+	"reflect"
+	"regexp"
+	"strconv"
+	"strings"
+	"time"
+)
+
+const (
+	IntParam = 1
+	StrParam = 2
+)
+
+type ControllerInterface interface {
+	Init()
+	DefaultAction() string
+	SetEnv(env *env)
+	PreFilter()
+	Render()
+	SetContext(ctxt ControllerInterface)
+	StartSession()
+	CloseSession()
+}
+
+type Error interface {
+	error
+	Type() string
+}
+
+type ErrorStruct struct {
+	typ     string
+	message string
+}
+
+type methodInfo struct {
+	name       string
+	method     reflect.Value
+	nparams    int
+	paramTypes []int
+}
+
+type controllerInfo struct {
+	name              string
+	controller        ControllerInterface
+	controllerType    reflect.Type
+	controllerPtrType reflect.Type
+	methodMap         map[string]*methodInfo
+}
+
+type Application struct {
+	controllerMap     map[string]*controllerInfo
+	defaultController string
+}
+
+type env struct {
+	path        string
+	controller  string
+	lcontroller string
+	action      string
+	laction     string
+	params      []string
+	request     *fastcgi.Request
+	body        string
+	form        map[string][]string
+	upload      map[string][](*Upload)
+	cookies     map[string]string
+}
+
+type Upload struct {
+	File     *os.File
+	Filename string
+}
+
+type cookie struct {
+	value    string
+	expire   time.Time
+	path     string
+	domain   string
+	secure   bool
+	httpOnly bool
+}
+
+type Controller struct {
+	Path        string
+	Name        string
+	LName       string
+	Action      string
+	LAction     string
+	Params      []string
+	PageTitle   string
+	Layout      string
+	ContentType string
+	Body        string
+	Form        map[string][]string
+	Upload      map[string][]*Upload
+	Cookies     map[string]string
+	Session     *Session
+	setCookies  map[string]*cookie
+	ctxt        ControllerInterface
+	Request     *fastcgi.Request
+	preRenered  bool
+}
+
+func NewError(typ string, message string) *ErrorStruct {
+	return &ErrorStruct{typ, message}
+}
+
+func (e *ErrorStruct) Error() string { return e.message }
+
+func (e *ErrorStruct) Type() string { return e.typ }
+
+func (c *Controller) Init() {
+	c.PageTitle = ""
+	c.Layout = "default"
+	c.ContentType = "text/html; charset=utf-8"
+}
+
+func (c *Controller) DefaultAction() string { return "Index" }
+
+func (c *Controller) SetEnv(env *env) {
+	c.Name = env.controller
+	c.LName = env.lcontroller
+	c.Action = env.action
+	c.LAction = env.laction
+	c.Path = env.path
+	c.Params = env.params
+	c.Request = env.request
+	c.Body = env.body
+	c.Form = env.form
+	c.Upload = env.upload
+	c.Cookies = env.cookies
+}
+
+func (c *Controller) PreFilter() {}
+
+type tmplInfo struct {
+	tmpl  *Template
+	mtime time.Time
+}
+
+var tmplCache map[string]*tmplInfo = make(map[string]*tmplInfo)
+
+func loadTemplate(fname string) (*Template, error) {
+	dir, e := os.Stat(fname)
+	if e != nil {
+		return nil, e
+	}
+	if !!dir.IsDir() {
+		return nil, NewError("Generic", "'"+fname+"' is not a regular file")
+	}
+	ti, _ := tmplCache[fname]
+	if ti == nil || dir.ModTime().After(ti.mtime) {
+		bytes, e := ioutil.ReadFile(fname)
+		if e != nil {
+			return nil, e
+		}
+		t, e := Parse(string(bytes), nil)
+		if e != nil {
+			return nil, e
+		}
+		ti = &tmplInfo{
+			mtime: dir.ModTime(),
+			tmpl:  t,
+		}
+		tmplCache[fname] = ti
+	}
+	return ti.tmpl, nil
+}
+
+func (c *Controller) SetContext(ctxt ControllerInterface) {
+	if c.ctxt == nil {
+		c.ctxt = ctxt
+	}
+}
+
+func (c *Controller) preRender() {
+	if !c.preRenered {
+		io.WriteString(c.Request.Stdout, "Content-Type: "+c.ContentType+"\r\n")
+
+		if c.setCookies != nil {
+			for k, ck := range c.setCookies {
+				s := "Set-Cookie: " + k + "=" + url.QueryEscape(ck.value)
+				if !ck.expire.IsZero() {
+					s += "; expire=" + ck.expire.Format(time.RFC1123)
+				}
+				if ck.path != "" {
+					s += "; path=" + ck.path
+				}
+				if ck.domain != "" {
+					s += "; path=" + ck.domain
+				}
+				if ck.secure {
+					s += "; secure"
+				}
+				if ck.httpOnly {
+					s += "; HttpOnly"
+				}
+				io.WriteString(c.Request.Stdout, s+"\r\n")
+			}
+		}
+
+		io.WriteString(c.Request.Stdout, "\r\n")
+		c.preRenered = true
+	}
+}
+
+func executeTemplate(fname string, t *Template, w io.Writer, data interface{}) {
+	e := t.Execute(w, data)
+	if e != nil {
+		log.Printf("error occurred during executing template %s: %s", fname, e)
+	}
+}
+
+func (c *Controller) RenderContent() string {
+	c.preRender()
+
+	fname := "views/" + c.LName + "/" + c.LAction + ".tpl"
+	t, e := loadTemplate(fname)
+	if e != nil {
+		// Dump c.ctxt contents
+	}
+
+	if t != nil {
+		executeTemplate(fname, t, c.Request.Stdout, c.ctxt)
+	}
+
+	return ""
+}
+
+func (c *Controller) renderTemplate(fname string) {
+	t, e := loadTemplate(fname)
+	if e == nil {
+		executeTemplate(fname, t, c.Request.Stdout, c.ctxt)
+	} else {
+		log.Printf("failed to load template %s: %s", fname, e)
+	}
+}
+
+func (c *Controller) RenderElement(name string) string {
+	c.renderTemplate("views/elements/" + name + ".tpl")
+	return ""
+}
+
+func (c *Controller) RenderControllerElement(name string) string {
+	c.renderTemplate("views/" + c.LName + "/elements/" + name + ".tpl")
+	return ""
+}
+
+func (c *Controller) Render() {
+	c.preRender()
+
+	if len(c.Layout) == 0 {
+		c.RenderContent()
+		return
+	}
+
+	fname := "views/layouts/" + c.Layout + ".tpl"
+	t, e := loadTemplate(fname)
+	if e != nil {
+		log.Printf("failed to load layout template %s: %s", fname, e)
+		c.RenderContent()
+	} else {
+		executeTemplate(fname, t, c.Request.Stdout, c.ctxt)
+	}
+}
+
+func (c *Controller) SetCookie(key string, value string) {
+	c.SetCookieFull(key, value, time.Time{}, "", "", false, false)
+}
+
+func (c *Controller) SetCookieFull(key string, value string, expire time.Time, path string, domain string, secure bool, httpOnly bool) {
+	if c.setCookies == nil {
+		c.setCookies = make(map[string]*cookie)
+	}
+
+	c.setCookies[key] = &cookie{
+		value:    value,
+		expire:   expire,
+		path:     path,
+		domain:   domain,
+		secure:   secure,
+		httpOnly: httpOnly,
+	}
+}
+
+func (c *Controller) StartSession() {
+	if c.Session != nil {
+		return
+	}
+
+	c.Session = GetSession(c)
+}
+
+func (c *Controller) CloseSession() {
+	if c.Session == nil {
+		return
+	}
+
+	c.Session.Close()
+}
+
+type ErrorHandler struct {
+	Controller
+	typ string
+}
+
+func NewErrorHandler(e Error, r *fastcgi.Request) *ErrorHandler {
+	eh := &ErrorHandler{
+		typ: e.Type(),
+	}
+	eh.Request = r
+	eh.Init()
+	eh.SetContext(eh)
+	return eh
+}
+
+func (eh *ErrorHandler) RenderContent() string {
+	eh.preRender()
+
+	fname := "views/errors/" + eh.typ + ".tpl"
+	t, e := loadTemplate(fname)
+	if e != nil {
+		var msg string
+		switch eh.typ {
+		case "PageNotFound":
+			msg = "Hmm, the page you’re looking for can't be found."
+		default:
+			msg = "We're sorry, but there was an error processing your request. Please try again later."
+		}
+		fmt.Fprintf(eh.Request.Stdout, "%s", msg)
+	} else {
+		t.Execute(eh.Request.Stdout, eh)
+		executeTemplate(fname, t, eh.Request.Stdout, eh)
+	}
+
+	return ""
+}
+
+func titleCase(s string) string {
+	if len(s) > 0 {
+		parts := strings.Split(s, "_")
+		for i, p := range parts {
+			l := len(p)
+			if l > 0 {
+				parts[i] = strings.ToUpper(string(p[0])) + p[1:len(p)]
+			} else {
+				parts[i] = ""
+			}
+		}
+		return strings.Join(parts, "")
+	}
+	return s
+}
+
+func deTitleCase(s string) string {
+	if len(s) > 0 {
+		var o string
+		for i, c := range s {
+			if c >= 'A' && c <= 'Z' {
+				if i > 0 {
+					o += "_"
+				}
+				o += strings.ToLower(string(c))
+			} else {
+				o += string(c)
+			}
+		}
+		return o
+	}
+	return s
+}
+
+func parseKeyValueString(m map[string][]string, s string) error {
+	if s == "" {
+		return nil
+	}
+
+	// copied from pkg/http/request.go
+	for _, kv := range strings.Split(s, "&") {
+		if kv == "" {
+			continue
+		}
+		kvPair := strings.SplitN(kv, "=", 2)
+
+		var key, value string
+		var e error
+		key, e = url.QueryUnescape(kvPair[0])
+		if e == nil && len(kvPair) > 1 {
+			value, e = url.QueryUnescape(kvPair[1])
+		}
+		if e != nil {
+			return e
+		}
+
+		vec, ok := m[key]
+		if !ok {
+			vec = make([]string, 1)
+		}
+		m[key] = append(vec, value)
+	}
+
+	return nil
+}
+
+var boundaryRE, _ = regexp.Compile("boundary=\"?([^\";,]+)\"?")
+
+type multipartReader struct {
+	rd   io.Reader
+	bd   []byte
+	buf  []byte
+	head int
+	tail int
+	eof  bool
+	done bool
+}
+
+func newMultipartReader(rd io.Reader, bd string) *multipartReader {
+	return &multipartReader{
+		rd:   rd,
+		bd:   []byte("\r\n--" + bd),
+		buf:  make([]byte, 4096),
+		head: 0,
+		tail: 0,
+	}
+}
+
+func (md *multipartReader) finished() bool { return md.done }
+
+func (md *multipartReader) read(delim []byte) ([]byte, error) {
+	if md.done {
+		return nil, io.EOF
+	}
+
+	if !md.eof && md.tail < len(md.buf) {
+		n, e := md.rd.Read(md.buf[md.tail:len(md.buf)])
+		if e != nil {
+			if e != io.EOF {
+				return nil, e
+			}
+			md.eof = true
+		}
+		md.tail += n
+	}
+
+	if i := bytes.Index(md.buf[md.head:md.tail], delim); i >= 0 {
+		s := make([]byte, i)
+		copy(s, md.buf[md.head:md.head+i])
+		md.head += i + len(delim)
+		return s, io.EOF
+	}
+
+	if md.eof {
+		md.done = true
+		return md.buf[md.head:md.tail], io.EOF
+	}
+
+	bf := md.tail - md.head
+	keep := len(delim) - 1
+	if keep > bf {
+		keep = bf
+	}
+	stop := md.tail - keep
+	n := stop - md.head
+	s := make([]byte, n)
+	if n > 0 {
+		copy(s, md.buf[md.head:stop])
+	}
+
+	copy(md.buf[0:keep], md.buf[stop:md.tail])
+	md.head = 0
+	md.tail = keep
+
+	return s, nil
+}
+
+func (md *multipartReader) readFirstLine() { md.readStringUntil(md.bd[2:len(md.bd)], false) }
+
+var crlf2 = []byte{'\r', '\n', '\r', '\n'}
+
+type byteConsumer func([]byte) error
+
+func (md *multipartReader) readUntil(delim []byte, checkEnd bool, f byteConsumer) error {
+	for {
+		b, e := md.read(delim)
+		if b != nil {
+			if e := f(b); e != nil {
+				return e
+			}
+		}
+		if e != nil {
+			if e == io.EOF {
+				if checkEnd && md.tail-md.head >= 2 && string(md.buf[md.head:md.head+2]) == "--" {
+					md.done = true
+				}
+				break
+			}
+			return e
+		}
+	}
+
+	return nil
+}
+
+func (md *multipartReader) readStringUntil(delim []byte, checkEnd bool) (string, error) {
+	var s string
+	e := md.readUntil(delim, checkEnd, func(b []byte) error {
+		s += string(b)
+		return nil
+	})
+	return s, e
+}
+
+type hdrInfo struct {
+	key     string
+	val     string
+	attribs map[string]string
+}
+
+func parseHeader(line string) *hdrInfo {
+	var key, attrib string
+	var hdr *hdrInfo
+	var attribs map[string]string
+	var j int
+	phase := 0
+	line += ";"
+	for i, c := range line {
+		switch phase {
+		case 0:
+			if c == ':' {
+				key = strings.TrimSpace(line[0:i])
+				phase++
+				j = i + 1
+			}
+		case 1:
+			if c == ';' {
+				attribs = make(map[string]string)
+				hdr = &hdrInfo{
+					key:     key,
+					val:     strings.TrimSpace(line[j:i]),
+					attribs: attribs,
+				}
+				phase++
+				j = i + 1
+			}
+		case 2:
+			if c == '=' {
+				attrib = strings.TrimSpace(line[j:i])
+				phase++
+				j = i + 1
+			}
+		case 3:
+			if c == '"' {
+				phase++
+				j = i + 1
+			} else if c == ';' {
+				attribs[attrib] = strings.TrimSpace(line[j:i])
+				phase = 2
+				j = i + 1
+			}
+		case 4:
+			if c == '\\' {
+				phase++
+			} else if c == '"' {
+				attribs[attrib] = line[j:i]
+				phase += 2
+			}
+		case 5:
+			phase--
+		case 6:
+			if c == ';' {
+				phase = 2
+				j = i + 1
+			}
+		}
+	}
+	return hdr
+}
+
+func (md *multipartReader) readHeaders() (map[string]*hdrInfo, error) {
+	s, _ := md.readStringUntil(crlf2, false)
+	lines := strings.Split(s[2:len(s)], "\r\n")
+	hdrs := make(map[string]*hdrInfo)
+	for _, line := range lines {
+		if hdr := parseHeader(line); hdr != nil {
+			hdrs[hdr.key] = hdr
+		}
+	}
+	return hdrs, nil
+}
+
+func (md *multipartReader) readBody() (string, error) {
+	return md.readStringUntil(md.bd, true)
+}
+
+var pads = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+
+func tempfile() (*os.File, error) {
+	tmpdir := os.Getenv("TMPDIR")
+	if tmpdir == "" {
+		tmpdir = "/tmp"
+	}
+
+	for {
+		var s string
+		for i := 0; i < 10; i++ {
+			s += string(pads[rand.Int()%len(pads)])
+		}
+		file, e := os.OpenFile(tmpdir+"/fastweb."+s, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
+		if e == nil {
+			return file, e
+		}
+		pe, ok := e.(*os.PathError)
+		if !ok || !os.IsExist(pe.Err) {
+			return nil, e
+		}
+	}
+
+	return nil, nil
+}
+
+func parseMultipartForm(m map[string][]string, u map[string][]*Upload, r *fastcgi.Request) error {
+	ct := r.Params["CONTENT_TYPE"]
+	a := boundaryRE.FindStringSubmatchIndex(ct)
+	if len(a) < 4 {
+		return errors.New("can't find boundary in content type")
+	}
+	b := ct[a[2]:a[3]]
+	md := newMultipartReader(r.Stdin, b)
+	md.readFirstLine()
+	for !md.finished() {
+		hdrs, e := md.readHeaders()
+		if e != nil {
+			return e
+		}
+		cd, ok := hdrs["Content-Disposition"]
+		if !ok {
+			return errors.New("can't find Content-Disposition")
+		}
+		name, ok := cd.attribs["name"]
+		if !ok {
+			return errors.New("can't find attrib 'name' in Content-Disposition")
+		}
+		filename, ok := cd.attribs["filename"]
+		if ok {
+			vec, ok := u[name]
+			if !ok {
+				vec = make([]*Upload, 1)
+				u[name] = vec
+			}
+
+			file, e := tempfile()
+			if e != nil {
+				return e
+			}
+			wr := bufio.NewWriter(file)
+			fname := file.Name()
+			md.readUntil(md.bd, true, func(b []byte) error {
+				if _, e := wr.Write(b); e != nil {
+					return e
+				}
+				return nil
+			})
+			wr.Flush()
+			// to flush (system) buffer, re-open immediately
+			file.Close()
+			file, _ = os.Open(fname)
+
+			vec = append(vec, &Upload{
+				File:     file,
+				Filename: filename,
+			})
+		} else {
+			vec, ok := m[name]
+			if !ok {
+				vec = make([]string, 1)
+				m[name] = vec
+			}
+			s, e := md.readBody()
+			if e != nil {
+				return e
+			}
+			vec = append(vec, s)
+		}
+	}
+	return nil
+}
+
+func parseForm(r *fastcgi.Request) (string, map[string][]string, map[string][]*Upload, error) {
+	m := make(map[string][]string)
+	u := make(map[string][]*Upload)
+	var body string
+
+	s := r.Params["QUERY_STRING"]
+	if s != "" {
+		e := parseKeyValueString(m, s)
+		if e != nil {
+			return body, nil, nil, e
+		}
+	}
+
+	if r.Params["REQUEST_METHOD"] == "POST" {
+		switch ct := r.Params["CONTENT_TYPE"]; true {
+		case strings.HasPrefix(ct, "application/x-www-form-urlencoded") && (len(ct) == 33 || ct[33] == ';'):
+			var b []byte
+			var e error
+			if b, e = ioutil.ReadAll(r.Stdin); e != nil {
+				return body, nil, nil, e
+			}
+			body = string(b)
+			e = parseKeyValueString(m, body)
+			if e != nil {
+				return body, nil, nil, e
+			}
+		case strings.HasPrefix(ct, "multipart/form-data"):
+			e := parseMultipartForm(m, u, r)
+			if e != nil {
+				return body, nil, nil, e
+			}
+		default:
+			log.Printf("unknown content type '%s'", ct)
+		}
+	}
+
+	form := make(map[string][]string)
+	for k, vec := range m {
+		form[k] = make([]string, len(vec))
+		copy(form[k], vec)
+	}
+
+	upload := make(map[string][]*Upload)
+	for k, vec := range u {
+		d := make([]*Upload, len(vec))
+		copy(d, vec)
+		v := make([]*Upload, len(d))
+		for i, u := range d {
+			v[i] = u
+		}
+		upload[k] = v
+	}
+
+	return body, form, upload, nil
+}
+
+func parseCookies(r *fastcgi.Request) (map[string]string, error) {
+	cookies := make(map[string]string)
+
+	if s, ok := r.Params["HTTP_COOKIE"]; ok {
+		var key string
+		phase := 0
+		j := 0
+		s += ";"
+		for i, c := range s {
+			switch phase {
+			case 0:
+				if c == '=' {
+					key = strings.TrimSpace(s[j:i])
+					j = i + 1
+					phase++
+				}
+			case 1:
+				if c == ';' {
+					v, e := url.QueryUnescape(s[j:i])
+					if e != nil {
+						return cookies, e
+					}
+					cookies[key] = v
+					phase = 0
+					j = i + 1
+				}
+			}
+		}
+	}
+
+	return cookies, nil
+}
+
+func (a *Application) getEnv(r *fastcgi.Request) *env {
+	var params []string
+	var lname string
+	var laction string
+
+	path, _ := r.Params["REQUEST_URI"]
+	p := strings.SplitN(path, "?", 2)
+	if len(p) > 1 {
+		path = p[0]
+		r.Params["QUERY_STRING"] = p[1]
+	}
+
+	pparts := strings.Split(path, "/")
+	n := len(pparts)
+	if n > 1 {
+		lname = pparts[1]
+		if n > 2 {
+			laction = pparts[2]
+			if n > 3 {
+				if pparts[n-1] == "" {
+					n--
+				}
+				params = pparts[3:n]
+			}
+		}
+	}
+
+	name := titleCase(lname)
+	action := titleCase(laction)
+
+	body, form, upload, e := parseForm(r)
+	if e != nil {
+		log.Printf("failed to parse form: %s", e.Error())
+	}
+
+	cookies, e := parseCookies(r)
+	if e != nil {
+		log.Printf("failed to parse cookies: %s", e.Error())
+	}
+
+	return &env{
+		path:        path,
+		controller:  name,
+		lcontroller: lname,
+		action:      action,
+		laction:     laction,
+		params:      params,
+		request:     r,
+		body:        body,
+		form:        form,
+		upload:      upload,
+		cookies:     cookies,
+	}
+}
+
+func (a *Application) route(r *fastcgi.Request) error {
+	env := a.getEnv(r)
+
+	if env.controller == "" {
+		env.controller = a.defaultController
+		env.lcontroller = deTitleCase(env.controller)
+	}
+
+	cinfo, _ := a.controllerMap[env.controller]
+	if cinfo == nil {
+		return NewError("PageNotFound", "controller class '"+env.controller+"' not found")
+	}
+
+	vc := reflect.New(cinfo.controllerType)
+	vi := reflect.New(cinfo.controllerPtrType).Elem()
+	vi.Set(vc.Elem().Addr())
+	c := vi.Interface().(ControllerInterface)
+
+	if env.action == "" {
+		env.action = c.DefaultAction()
+		env.laction = deTitleCase(env.action)
+	}
+
+	minfo, _ := cinfo.methodMap[env.action]
+	if minfo == nil {
+		return NewError("PageNotFound", "action '"+env.action+"' is not implemented in controller '"+env.controller+"'")
+	}
+
+	if minfo.nparams > len(env.params) {
+		return NewError("PageNotFound", "not enough parameter")
+	}
+
+	pv := make([]reflect.Value, minfo.nparams+1)
+	pv[0] = vc
+
+	for i := 0; i < minfo.nparams; i++ {
+		p := env.params[i]
+		switch minfo.paramTypes[i] {
+		case StrParam:
+			pv[i+1] = reflect.ValueOf(p)
+		case IntParam:
+			x, e2 := strconv.Atoi(p)
+			if e2 != nil {
+				return NewError("PageNotFound", fmt.Sprintf("parameter %d must be an integer, input: %s", i+1, p))
+			}
+			pv[i+1] = reflect.ValueOf(x)
+		}
+	}
+
+	c.Init()
+	c.SetEnv(env)
+
+	c.PreFilter()
+
+	eval := minfo.method.Call(pv)[0]
+	if !eval.IsNil() {
+		elemval := eval.Elem()
+		return elemval.Interface().(error)
+	}
+
+	c.SetContext(c)
+	c.Render()
+
+	c.CloseSession()
+
+	return nil
+}
+
+func (a *Application) Handle(r *fastcgi.Request) bool {
+	e := a.route(r)
+
+	if e != nil {
+		var ee Error
+		if e, ok := e.(Error); ok {
+			ee = e
+		} else {
+			ee = NewError("Generic", e.Error())
+		}
+		log.Printf("%s", e.Error())
+		eh := NewErrorHandler(ee, r)
+		eh.Render()
+	}
+
+	return true
+}
+
+func NewApplication() *Application {
+	return &Application{
+		controllerMap:     make(map[string]*controllerInfo),
+		defaultController: "Default",
+	}
+}
+
+func (a *Application) RegisterController(c ControllerInterface) {
+	v := reflect.ValueOf(c)
+	pt := v.Type()
+	t := v.Elem().Type()
+	name := t.Name()
+
+	mmap := make(map[string]*methodInfo)
+
+	n := pt.NumMethod()
+	for i := 0; i < n; i++ {
+		m := pt.Method(i)
+		name := m.Name
+		switch name {
+		case "SetEnv", "Render", "DefaultAction", "Init", "PreFilter", "SetContext":
+			continue
+		}
+		mt := m.Type
+		if mt.NumOut() != 1 {
+			continue
+		}
+		mo := mt.Out(0)
+		switch mo.Kind() {
+		case reflect.Interface:
+			if mo.PkgPath() != "os" || mo.Name() != "Error" {
+				continue
+			}
+		default:
+			continue
+		}
+		nin := mt.NumIn() - 1
+		ptypes := make([]int, nin)
+		for j := 0; j < nin; j++ {
+			in := mt.In(j + 1)
+			switch in.Kind() {
+			case reflect.Int:
+				ptypes[j] = IntParam
+			case reflect.String:
+				ptypes[j] = StrParam
+			default:
+				continue
+			}
+		}
+		mmap[name] = &methodInfo{
+			name:       name,
+			method:     m.Func,
+			nparams:    nin,
+			paramTypes: ptypes,
+		}
+	}
+
+	a.controllerMap[name] = &controllerInfo{
+		name:              name,
+		controller:        c,
+		controllerType:    t,
+		controllerPtrType: pt,
+		methodMap:         mmap,
+	}
+}
+
+func (a *Application) Run(addr string) error {
+	rand.Seed(time.Now().UnixNano())
+	return fastcgi.RunStandalone(addr, a)
+}
+// 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.
+
+// Template library: default formatters
+
+package fastweb
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+)
+
+// StringFormatter formats into the default string representation.
+// It is stored under the name "str" and is the default formatter.
+// You can override the default formatter by storing your default
+// under the name "" in your custom formatter map.
+func StringFormatter(w io.Writer, format string, value ...interface{}) {
+	if len(value) == 1 {
+		if b, ok := value[0].([]byte); ok {
+			w.Write(b)
+			return
+		}
+	}
+	fmt.Fprint(w, value...)
+}
+
+var (
+	esc_quot = []byte("&#34;") // shorter than "&quot;"
+	esc_apos = []byte("&#39;") // shorter than "&apos;"
+	esc_amp  = []byte("&amp;")
+	esc_lt   = []byte("&lt;")
+	esc_gt   = []byte("&gt;")
+)
+
+// HTMLEscape writes to w the properly escaped HTML equivalent
+// of the plain text data s.
+func HTMLEscape(w io.Writer, s []byte) {
+	var esc []byte
+	last := 0
+	for i, c := range s {
+		switch c {
+		case '"':
+			esc = esc_quot
+		case '\'':
+			esc = esc_apos
+		case '&':
+			esc = esc_amp
+		case '<':
+			esc = esc_lt
+		case '>':
+			esc = esc_gt
+		default:
+			continue
+		}
+		w.Write(s[last:i])
+		w.Write(esc)
+		last = i + 1
+	}
+	w.Write(s[last:])
+}
+
+// HTMLFormatter formats arbitrary values for HTML
+func HTMLFormatter(w io.Writer, format string, value ...interface{}) {
+	ok := false
+	var b []byte
+	if len(value) == 1 {
+		b, ok = value[0].([]byte)
+	}
+	if !ok {
+		var buf bytes.Buffer
+		fmt.Fprint(&buf, value...)
+		b = buf.Bytes()
+	}
+	HTMLEscape(w, b)
+}
+
+var (
+	jesc_quot = []byte("\\\"") // shorter than "&quot;"
+	jesc_apos = []byte("\\'") // shorter than "&apos;"
+	jesc_bs = []byte("\\\\")
+	jesc_tab = []byte("\\t")
+	jesc_cr = []byte("\\r")
+	jesc_lf = []byte("\\n")
+)
+
+// JSONEscape writes to w the properly escaped JSON equivalent
+// of the plain text data s.
+func JSONEscape(w io.Writer, s []byte) {
+	var esc []byte
+	last := 0
+	for i, c := range s {
+		switch c {
+		case '"':
+			esc = jesc_quot
+		case '\'':
+			esc = jesc_apos
+		case '\\':
+			esc = jesc_bs
+		case '\t':
+			esc = jesc_tab
+		case '\r':
+			esc = jesc_cr
+		case '\n':
+			esc = jesc_lf
+		default:
+			continue
+		}
+		w.Write(s[last:i])
+		w.Write(esc)
+		last = i + 1
+	}
+	w.Write(s[last:])
+}
+
+// JSONFormatter formats arbitrary values for HTML
+func JSONFormatter(w io.Writer, format string, value ...interface{}) {
+	ok := false
+	var b []byte
+	if len(value) == 1 {
+		b, ok = value[0].([]byte)
+	}
+	if !ok {
+		var buf bytes.Buffer
+		fmt.Fprint(&buf, value...)
+		b = buf.Bytes()
+	}
+	JSONEscape(w, b)
+}
+// 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 fastweb
+
+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 TemplateError struct {
+	Line int
+	Msg  string
+}
+
+func (e *TemplateError) 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.(*TemplateError); 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,
+	"json": JSONFormatter,
+	"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(&TemplateError{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(&TemplateError{t.linenum, fmt.Sprintf(err, args...)})
+}
+
+// Is this an exported - upper case - name?
+func isExported(name string) bool {
+	rune, _ := utf8.DecodeRuneInString(name)
+	return unicode.IsUpper(rune)
+}
+
+// -- 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] = []byte(v)[0]
+			} 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)
+