go.talks/present: add .html function

R=r
CC=golang-dev
https://golang.org/cl/6726052
diff --git a/present/code.go b/present/code.go
index c60aedf..aa49ee2 100644
--- a/present/code.go
+++ b/present/code.go
@@ -81,19 +81,10 @@
 		SourceLine: lineno}, nil
 }
 
-// contents reads a file by name and returns its contents as a byte slice.
-func contents(name string) ([]byte, error) {
-	file, err := ioutil.ReadFile(name)
-	if err != nil {
-		return nil, err
-	}
-	return file, 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)
-	textBytes, err := contents(filename)
+	textBytes, err := ioutil.ReadFile(filename)
 	if err != nil {
 		return "", fmt.Errorf("%s:%d: %v", c.SourceFile, c.SourceLine, err)
 	}
@@ -182,7 +173,7 @@
 
 // oneLine returns the single line generated by a two-argument code invocation.
 func oneLine(file, text string, arg interface{}) (line, before, after string, err error) {
-	contentBytes, err := contents(file)
+	contentBytes, err := ioutil.ReadFile(file)
 	if err != nil {
 		return "", "", "", err
 	}
@@ -206,7 +197,7 @@
 
 // multipleLines returns the text generated by a three-argument code invocation.
 func multipleLines(file string, arg1, arg2 interface{}) (line, before, after string, err error) {
-	contentBytes, err := contents(file)
+	contentBytes, err := ioutil.ReadFile(file)
 	lines := strings.SplitAfter(string(contentBytes), "\n")
 	if err != nil {
 		return "", "", "", err
diff --git a/present/doc.go b/present/doc.go
index 4f368d7..00b3686 100644
--- a/present/doc.go
+++ b/present/doc.go
@@ -59,6 +59,7 @@
 	.play y.go
 	.image image.jpg
 	.link http://foo label
+	.html file.html
 
 	Again, more text
 
@@ -145,6 +146,7 @@
 Create a hyperlink. The syntax is 1 or 2 space-separated arguments.
 The first argument is always the HTTP URL.  If there is a second
 argument, it is the text label to display for this link.
+
 	.link http://golang.org golang.org
 
 image:
@@ -158,5 +160,13 @@
 
 	.image images/betsy.jpg 100 200
 
+html:
+
+The function html includes the contents of the specified file as
+unescaped HTML. This is useful for including custom HTML elements
+that cannot be created using only the slide format.
+
+	.html file.html
+
 */
 package main
diff --git a/present/html.go b/present/html.go
new file mode 100644
index 0000000..4c893c3
--- /dev/null
+++ b/present/html.go
@@ -0,0 +1,32 @@
+package main
+
+import (
+	"errors"
+	"html/template"
+	"io/ioutil"
+	"path/filepath"
+	"strings"
+)
+
+func init() {
+	Register("html", parseHTML, nil)
+}
+
+func parseHTML(fileName string, lineno int, text string) (Elem, error) {
+	p := strings.Fields(text)
+	if len(p) != 2 {
+		return nil, errors.New("invalid .html args")
+	}
+	name := filepath.Join(filepath.Dir(fileName), p[1])
+	b, err := ioutil.ReadFile(name)
+	if err != nil {
+		return nil, err
+	}
+	return HTML(b), nil
+}
+
+type HTML string
+
+func (s HTML) HTML(*template.Template) (template.HTML, error) {
+	return template.HTML(s), nil
+}
diff --git a/present/parse.go b/present/parse.go
index 108b9b1..327d103 100644
--- a/present/parse.go
+++ b/present/parse.go
@@ -11,6 +11,7 @@
 	"fmt"
 	"html/template"
 	"io"
+	"io/ioutil"
 	"log"
 	"net/url"
 	"path/filepath"
@@ -35,8 +36,10 @@
 	if len(name) == 0 || name[0] == ';' {
 		panic("bad name in Register: " + name)
 	}
-	funcs[name] = function
 	parsers["."+name] = parser
+	if function != nil {
+		funcs[name] = function
+	}
 }
 
 // renderSlides reads the slide file, builds its template representation,
@@ -143,7 +146,7 @@
 }
 
 func readLines(name string) (*Lines, error) {
-	contentBytes, err := contents(name)
+	contentBytes, err := ioutil.ReadFile(name)
 	if err != nil {
 		return nil, err
 	}