go.talks/pkg/present: refactor to remove action template functions

Now the Parse function reads all source files (including included
code files), and the template execution only renders templates.
(Previously, the two stages were confused.)

R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/6863049
diff --git a/pkg/present/code.go b/pkg/present/code.go
index 7783465..43b3867 100644
--- a/pkg/present/code.go
+++ b/pkg/present/code.go
@@ -9,7 +9,6 @@
 	"html/template"
 	"io/ioutil"
 	"log"
-	"path"
 	"path/filepath"
 	"regexp"
 	"strconv"
@@ -26,24 +25,16 @@
 // templates.
 
 func init() {
-	Register("code", parseCode, code)
-	Register("play", parseCode, code)
+	Register("code", parseCode)
+	Register("play", parseCode)
 }
 
 type Code struct {
-	Play       bool   // runnable code
-	File       string // file name to read input from
-	Cmd        string // text of input line
-	Addr       string // really an address
-	Highlight  string // HLxxx marker on end of line.
-	Type       string // type extension of file (.go etc.).
-	SourceFile string
-	SourceLine int
+	Text template.HTML
+	Play bool // runnable code
 }
 
-func (c Code) HTML(t *template.Template) (template.HTML, error) {
-	return execTemplate(t, "code", c)
-}
+func (c Code) TemplateName() string { return "code" }
 
 // The input line is a .code or .play entry with a file name and an optional HLfoo marker on the end.
 // Anything between the file and HL (if any) is an address expression, which we treat as a string here.
@@ -51,54 +42,43 @@
 var highlightRE = regexp.MustCompile(`\s+HL([a-zA-Z0-9_]+)?$`)
 var codeRE = regexp.MustCompile(`\.(code|play)\s+([^\s]+)(\s+)?(.*)?$`)
 
-func parseCode(fileName string, lineno int, text string) (Elem, error) {
-	text = strings.TrimSpace(text)
+func parseCode(sourceFile string, sourceLine int, cmd string) (Elem, error) {
+	cmd = strings.TrimSpace(cmd)
+
 	// Pull off the HL, if any, from the end of the input line.
 	highlight := ""
-	if hl := highlightRE.FindStringSubmatchIndex(text); len(hl) == 4 {
-		highlight = text[hl[2]:hl[3]]
-		text = text[:hl[2]-2]
+	if hl := highlightRE.FindStringSubmatchIndex(cmd); len(hl) == 4 {
+		highlight = cmd[hl[2]:hl[3]]
+		cmd = cmd[:hl[2]-2]
 	}
+
 	// Parse the remaining command line.
-	args := codeRE.FindStringSubmatch(text)
 	// Arguments:
 	// args[0]: whole match
 	// args[1]:  .code/.play
 	// args[2]: file name
 	// args[3]: space, if any, before optional address
 	// args[4]: optional address
+	args := codeRE.FindStringSubmatch(cmd)
 	if len(args) != 5 {
-		return nil, fmt.Errorf("%s:%d: syntax error for .code/.play invocation", fileName, lineno)
+		return nil, fmt.Errorf("%s:%d: syntax error for .code/.play invocation", sourceFile, sourceLine)
 	}
 	command, file, addr := args[1], args[2], strings.TrimSpace(args[4])
+	play := command == "play" && PlayEnabled
 
-	typ := path.Ext(fileName)
-	for len(typ) > 0 && typ[0] == '.' {
-		typ = typ[1:]
-	}
-	return Code{
-		Play:       command == "play" && PlayEnabled,
-		File:       file,
-		Cmd:        text,
-		Addr:       addr,
-		Highlight:  highlight,
-		Type:       typ,
-		SourceFile: fileName,
-		SourceLine: lineno}, nil
-}
-
-// code is the entry point for the '.code' present command.
-func code(c Code) (template.HTML, error) {
-	filename := filepath.Join(filepath.Dir(c.SourceFile), c.File)
+	// Read in code file and (optionally) match address.
+	filename := filepath.Join(filepath.Dir(sourceFile), file)
 	textBytes, err := ioutil.ReadFile(filename)
 	if err != nil {
-		return "", fmt.Errorf("%s:%d: %v", c.SourceFile, c.SourceLine, err)
+		return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err)
 	}
-	lo, hi, err := addrToByteRange(c.Addr, 0, textBytes)
+	lo, hi, err := addrToByteRange(addr, 0, textBytes)
 	if err != nil {
-		return "", fmt.Errorf("%s:%d: %v", c.SourceFile, c.SourceLine, err)
+		return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err)
 	}
-	// Acme patterns stop mid-line, so run to end of line in both directions.
+
+	// Acme pattern matches stop mid-line,
+	// so run to end of line in both directions.
 	for lo > 0 && textBytes[lo-1] != '\n' {
 		lo--
 	}
@@ -108,25 +88,35 @@
 			break
 		}
 	}
-	text := skipOMIT(textBytes[lo:hi])
+	text := string(textBytes[lo:hi])
+
+	// Clear ommitted lines.
+	text = skipOMIT(text)
+
 	// 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)
+
 	// Highlight and span-wrap lines.
-	text = "<pre>" + highlightLines(text, c.Highlight) + "</pre>"
+	text = "<pre>" + highlightLines(text, highlight) + "</pre>"
+
 	// Include before and after in a hidden span for playground code.
-	if c.Play {
-		text = hide(skipOMIT(textBytes[:lo])) + text + hide(skipOMIT(textBytes[hi:]))
+	if play {
+		text = hide(skipOMIT(string(textBytes[:lo]))) +
+			text + hide(skipOMIT(string(textBytes[hi:])))
 	}
+
 	// Include the command as a comment.
-	text = fmt.Sprintf("<!--{{%s}}\n-->%s", c.Cmd, text)
-	return template.HTML(text), nil
+	text = fmt.Sprintf("<!--{{%s}}\n-->%s", cmd, text)
+
+	return Code{Text: template.HTML(text), Play: play}, nil
 }
 
 // skipOMIT turns text into a string, dropping lines ending with OMIT.
-func skipOMIT(text []byte) string {
-	lines := strings.SplitAfter(string(text), "\n")
+func skipOMIT(text string) string {
+	lines := strings.SplitAfter(text, "\n")
 	for k := range lines {
 		if strings.HasSuffix(lines[k], "OMIT\n") {
 			lines[k] = ""
diff --git a/pkg/present/html.go b/pkg/present/html.go
index 8943e13..9fce270 100644
--- a/pkg/present/html.go
+++ b/pkg/present/html.go
@@ -9,7 +9,7 @@
 )
 
 func init() {
-	Register("html", parseHTML, nil)
+	Register("html", parseHTML)
 }
 
 func parseHTML(fileName string, lineno int, text string) (Elem, error) {
@@ -22,11 +22,11 @@
 	if err != nil {
 		return nil, err
 	}
-	return HTML(b), nil
+	return HTML{template.HTML(b)}, nil
 }
 
-type HTML string
-
-func (s HTML) HTML(*template.Template) (template.HTML, error) {
-	return template.HTML(s), nil
+type HTML struct {
+	template.HTML
 }
+
+func (s HTML) TemplateName() string { return "html" }
diff --git a/pkg/present/image.go b/pkg/present/image.go
index 9f7dc8d..f402e5a 100644
--- a/pkg/present/image.go
+++ b/pkg/present/image.go
@@ -11,37 +11,31 @@
 )
 
 func init() {
-	Register("image", parseImage, image)
+	Register("image", parseImage)
 }
 
 type Image struct {
-	File string
-	Args []interface{}
+	URL        string
+	Attributes template.HTML
 }
 
-func (i Image) HTML(t *template.Template) (template.HTML, error) {
-	return execTemplate(t, "image", i)
-}
+func (i Image) TemplateName() string { return "image" }
 
 func parseImage(fileName string, lineno int, text string) (Elem, error) {
 	args := strings.Fields(text)
+	img := Image{URL: args[1]}
 	a, err := parseArgs(fileName, lineno, args[2:])
 	if err != nil {
 		return nil, err
 	}
-	return Image{File: args[1], Args: a}, nil
-}
-
-// image is the entry point for the '.image' present command.
-func image(file string, arg []interface{}) (template.HTML, error) {
-	args := ""
-	switch len(arg) {
+	switch len(a) {
 	case 0:
 		// no size parameters
 	case 2:
-		args = fmt.Sprintf("height='%v' width='%v'", arg[0], arg[1])
+		attr := fmt.Sprintf(`height="%v" width="%v"`, a[0], a[1])
+		img.Attributes = template.HTML(attr)
 	default:
-		return "", fmt.Errorf("incorrect image invocation: code %q %v", file, arg)
+		return nil, fmt.Errorf("incorrect image invocation: %q", text)
 	}
-	return template.HTML(fmt.Sprintf(`<img src=%q %s>`, file, args)), nil
+	return img, nil
 }
diff --git a/pkg/present/link.go b/pkg/present/link.go
index cab1a03..1e00ef1 100644
--- a/pkg/present/link.go
+++ b/pkg/present/link.go
@@ -6,23 +6,20 @@
 
 import (
 	"fmt"
-	"html/template"
 	"net/url"
 	"strings"
 )
 
 func init() {
-	Register("link", parseLink, link)
+	Register("link", parseLink)
 }
 
 type Link struct {
-	URL  *url.URL
-	Args []string
+	URL   *url.URL
+	Label string
 }
 
-func (l Link) HTML(t *template.Template) (template.HTML, error) {
-	return execTemplate(t, "link", l)
-}
+func (l Link) TemplateName() string { return "link" }
 
 func parseLink(fileName string, lineno int, text string) (Elem, error) {
 	args := strings.Fields(text)
@@ -30,23 +27,17 @@
 	if err != nil {
 		return nil, err
 	}
-	return Link{url, args[2:]}, nil
-}
-
-// link is the entry point for the '.link' present command.
-func link(url url.URL, arg []string) (template.HTML, error) {
 	label := ""
-	switch len(arg) {
-	case 0:
+	if len(args) > 2 {
+		label = strings.Join(args[2:], " ")
+	} else {
 		scheme := url.Scheme + "://"
 		if url.Scheme == "mailto" {
 			scheme = "mailto:"
 		}
 		label = strings.Replace(url.String(), scheme, "", 1)
-	default:
-		label = strings.Join(arg, " ")
 	}
-	return template.HTML(renderLink(url.String(), label)), nil
+	return Link{url, label}, nil
 }
 
 func renderLink(url, text string) string {
diff --git a/pkg/present/parse.go b/pkg/present/parse.go
index b8d9eea..e75ed34 100644
--- a/pkg/present/parse.go
+++ b/pkg/present/parse.go
@@ -30,19 +30,16 @@
 	return template.New("").Funcs(funcs)
 }
 
+type ParseFunc func(fileName string, lineNumber int, inputLine string) (Elem, error)
+
 // Register binds the named action, which does not being with a period, to the
-// specified parser and template function to be invoked when the name, with a
-// period, appears in the present input text.
-// The function argument is an optional template function that is available
-// inside templates under that name.
-func Register(name string, parser func(fileName string, lineNumber int, inputLine string) (Elem, error), function interface{}) {
+// specified parser to be invoked when the name, with a period, appears in the
+// present input text.
+func Register(name string, parser ParseFunc) {
 	if len(name) == 0 || name[0] == ';' {
 		panic("bad name in Register: " + name)
 	}
 	parsers["."+name] = parser
-	if function != nil {
-		funcs[name] = function
-	}
 }
 
 // Doc represents an entire document.
@@ -107,14 +104,22 @@
 	return b.String()
 }
 
-func (s Section) HTML(tmpl *template.Template) (template.HTML, error) {
-	return execTemplate(tmpl, "section", s)
+func (s Section) TemplateName() string { return "section" }
+
+// Elem defines the interface for a present element. That is, something that
+// can provide the name of the template used to render the element.
+type Elem interface {
+	TemplateName() string
 }
 
-// Elem defines the interface for a present element.
-// That is, something that can render itself in HTML.
-type Elem interface {
-	HTML(t *template.Template) (template.HTML, error)
+// renderElem implements the elem template function, used to render
+// sub-templates.
+func renderElem(t *template.Template, e Elem) (template.HTML, error) {
+	return execTemplate(t, e.TemplateName(), e)
+}
+
+func init() {
+	funcs["elem"] = renderElem
 }
 
 // execTemplate is a helper to execute a template and return the output as a
@@ -134,18 +139,14 @@
 	Pre   bool
 }
 
-func (t Text) HTML(tmpl *template.Template) (template.HTML, error) {
-	return execTemplate(tmpl, "text", t)
-}
+func (t Text) TemplateName() string { return "text" }
 
 // List represents a bulleted list.
 type List struct {
 	Bullet []string
 }
 
-func (l List) HTML(t *template.Template) (template.HTML, error) {
-	return execTemplate(t, "list", l)
-}
+func (l List) TemplateName() string { return "list" }
 
 // Lines is a helper for parsing line-based input.
 type Lines struct {
@@ -393,7 +394,7 @@
 		case strings.HasPrefix(text, "@"):
 			el = parseURL("http://twitter.com/" + text[1:])
 			if l, ok := el.(Link); ok {
-				l.Args = []string{text}
+				l.Label = text
 				el = l
 			}
 		case strings.Contains(text, ":"):
diff --git a/present/templates/action.tmpl b/present/templates/action.tmpl
index 4be2c6e..d7943db 100644
--- a/present/templates/action.tmpl
+++ b/present/templates/action.tmpl
@@ -5,7 +5,7 @@
 
 {{define "section"}}
   <h{{len .Number}} id="TOC_{{.FormattedNumber}}">{{.FormattedNumber}} {{.Title}}</h{{len .Number}}>
-  {{range .Elem}}{{.HTML $.Doc.Template}}{{end}}
+  {{range .Elem}}{{elem $.Doc.Template .}}{{end}}
 {{end}}
 
 {{define "list"}}
@@ -28,9 +28,11 @@
 {{end}}
 
 {{define "code"}}
-  <div class="code{{if .Play}} playground{{end}}" contenteditable="true" spellcheck="false">{{code .}}</div>
+  <div class="code{{if .Play}} playground{{end}}" contenteditable="true" spellcheck="false">{{.Text}}</div>
 {{end}}
 
-{{define "image"}}<div class="image">{{image .File .Args}}</div>{{end}}
+{{define "image"}}<div class="image"><img src="{{.URL}}" {{.Attributes}}></div>{{end}}
       
-{{define "link"}}<p class="link">{{link .URL .Args}}</p>{{end}}
+{{define "link"}}<p class="link"><a href="{{.URL}}" target="_blank">{{style .Label}}</a></p>{{end}}
+
+{{define "html"}}{{.HTML}}{{end}}
diff --git a/present/templates/article.tmpl b/present/templates/article.tmpl
index 83e4817..01b86d1 100644
--- a/present/templates/article.tmpl
+++ b/present/templates/article.tmpl
@@ -26,13 +26,13 @@
         {{end}}
 
         {{range .Sections}}
-          {{.HTML $.Template}}
+          {{elem $.Template .}}
         {{end}}{{/* of Section block */}}
 
         <h2>Authors</h2>
         {{range .Authors}}
-            <div class="author">
-            {{range .Elem}}{{.HTML $.Template}}{{end}}
+          <div class="author">
+            {{range .Elem}}{{elem $.Template .}}{{end}}
           </div>
         {{end}}
       </div>
diff --git a/present/templates/slides.tmpl b/present/templates/slides.tmpl
index 7696971..1e69017 100644
--- a/present/templates/slides.tmpl
+++ b/present/templates/slides.tmpl
@@ -19,7 +19,7 @@
         {{if not .Time.IsZero}}<h3>{{.Time.Format "2 January 2006"}}</h3>{{end}}
         {{range .Authors}}
           <div class="presenter">
-            {{range .TextElem}}{{.HTML $.Template}}{{end}}
+            {{range .TextElem}}{{elem $.Template .}}{{end}}
           </div>
         {{end}}
       </article>
@@ -29,7 +29,7 @@
       <article>
       {{if $s.Elem}}
         <h3>{{$s.Title}}</h3>
-        {{range $s.Elem}}{{.HTML $.Template}}{{end}}
+        {{range $s.Elem}}{{elem $.Template .}}{{end}}
       {{else}}
         <h2>{{$s.Title}}</h2>
       {{end}}
@@ -41,7 +41,7 @@
         <h3>Thank you</h1>
         {{range .Authors}}
           <div class="presenter">
-            {{range .Elem}}{{.HTML $.Template}}{{end}}
+            {{range .Elem}}{{elem $.Template .}}{{end}}
           </div>
         {{end}}
       </article>