go.talks/pkg/present: access files through new Context type

R=adg
CC=golang-dev
https://golang.org/cl/7312072
diff --git a/pkg/present/code.go b/pkg/present/code.go
index e27c42b..6ab60ae 100644
--- a/pkg/present/code.go
+++ b/pkg/present/code.go
@@ -7,7 +7,6 @@
 import (
 	"fmt"
 	"html/template"
-	"io/ioutil"
 	"log"
 	"path/filepath"
 	"regexp"
@@ -42,7 +41,7 @@
 var highlightRE = regexp.MustCompile(`\s+HL([a-zA-Z0-9_]+)?$`)
 var codeRE = regexp.MustCompile(`\.(code|play)\s+([^\s]+)(\s+)?(.*)?$`)
 
-func parseCode(sourceFile string, sourceLine int, cmd string) (Elem, error) {
+func parseCode(ctx *Context, 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.
@@ -68,7 +67,7 @@
 
 	// Read in code file and (optionally) match address.
 	filename := filepath.Join(filepath.Dir(sourceFile), file)
-	textBytes, err := ioutil.ReadFile(filename)
+	textBytes, err := ctx.ReadFile(filename)
 	if err != nil {
 		return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err)
 	}
@@ -168,8 +167,8 @@
 }
 
 // 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 := ioutil.ReadFile(file)
+func oneLine(ctx *Context, file, text string, arg interface{}) (line, before, after string, err error) {
+	contentBytes, err := ctx.ReadFile(file)
 	if err != nil {
 		return "", "", "", err
 	}
@@ -192,8 +191,8 @@
 }
 
 // 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 := ioutil.ReadFile(file)
+func multipleLines(ctx *Context, file string, arg1, arg2 interface{}) (line, before, after string, err error) {
+	contentBytes, err := ctx.ReadFile(file)
 	lines := strings.SplitAfter(string(contentBytes), "\n")
 	if err != nil {
 		return "", "", "", err
diff --git a/pkg/present/html.go b/pkg/present/html.go
index 9fce270..cca90ef 100644
--- a/pkg/present/html.go
+++ b/pkg/present/html.go
@@ -3,7 +3,6 @@
 import (
 	"errors"
 	"html/template"
-	"io/ioutil"
 	"path/filepath"
 	"strings"
 )
@@ -12,13 +11,13 @@
 	Register("html", parseHTML)
 }
 
-func parseHTML(fileName string, lineno int, text string) (Elem, error) {
+func parseHTML(ctx *Context, 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)
+	b, err := ctx.ReadFile(name)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/present/iframe.go b/pkg/present/iframe.go
index b9f0e78..2f3c5e5 100644
--- a/pkg/present/iframe.go
+++ b/pkg/present/iframe.go
@@ -21,7 +21,7 @@
 
 func (i Iframe) TemplateName() string { return "iframe" }
 
-func parseIframe(fileName string, lineno int, text string) (Elem, error) {
+func parseIframe(ctx *Context, fileName string, lineno int, text string) (Elem, error) {
 	args := strings.Fields(text)
 	i := Iframe{URL: args[1]}
 	a, err := parseArgs(fileName, lineno, args[2:])
diff --git a/pkg/present/image.go b/pkg/present/image.go
index ec859ce..2bab429 100644
--- a/pkg/present/image.go
+++ b/pkg/present/image.go
@@ -21,7 +21,7 @@
 
 func (i Image) TemplateName() string { return "image" }
 
-func parseImage(fileName string, lineno int, text string) (Elem, error) {
+func parseImage(ctx *Context, fileName string, lineno int, text string) (Elem, error) {
 	args := strings.Fields(text)
 	img := Image{URL: args[1]}
 	a, err := parseArgs(fileName, lineno, args[2:])
diff --git a/pkg/present/link.go b/pkg/present/link.go
index 1e00ef1..d683f02 100644
--- a/pkg/present/link.go
+++ b/pkg/present/link.go
@@ -21,7 +21,7 @@
 
 func (l Link) TemplateName() string { return "link" }
 
-func parseLink(fileName string, lineno int, text string) (Elem, error) {
+func parseLink(ctx *Context, fileName string, lineno int, text string) (Elem, error) {
 	args := strings.Fields(text)
 	url, err := url.Parse(args[1])
 	if err != nil {
diff --git a/pkg/present/parse.go b/pkg/present/parse.go
index 80ed34f..05091a7 100644
--- a/pkg/present/parse.go
+++ b/pkg/present/parse.go
@@ -21,7 +21,7 @@
 )
 
 var (
-	parsers = make(map[string]func(string, int, string) (Elem, error))
+	parsers = make(map[string]ParseFunc)
 	funcs   = template.FuncMap{}
 )
 
@@ -40,7 +40,7 @@
 	return t.ExecuteTemplate(w, "root", data)
 }
 
-type ParseFunc func(fileName string, lineNumber int, inputLine string) (Elem, error)
+type ParseFunc func(ctx *Context, fileName string, lineNumber int, inputLine string) (Elem, error)
 
 // Register binds the named action, which does not begin with a period, to the
 // specified parser to be invoked when the name, with a period, appears in the
@@ -212,6 +212,12 @@
 	return
 }
 
+// A Context specifies the supporting context for parsing a presentation.
+type Context struct {
+	// ReadFile reads the file named by filename and returns the contents.
+	ReadFile func(filename string) ([]byte, error)
+}
+
 // ParseMode represents flags for the Parse function.
 type ParseMode int
 
@@ -220,8 +226,8 @@
 	TitlesOnly ParseMode = 1
 )
 
-// Parse parses the document in the file specified by name.
-func Parse(r io.Reader, name string, mode ParseMode) (*Doc, error) {
+// Parse parses a document from r.
+func (ctx *Context) Parse(r io.Reader, name string, mode ParseMode) (*Doc, error) {
 	doc := new(Doc)
 	lines, err := readLines(r)
 	if err != nil {
@@ -239,12 +245,19 @@
 		return nil, err
 	}
 	// Sections
-	if doc.Sections, err = parseSections(name, lines, []int{}, doc); err != nil {
+	if doc.Sections, err = parseSections(ctx, name, lines, []int{}, doc); err != nil {
 		return nil, err
 	}
 	return doc, nil
 }
 
+// Parse parses a document from r. Parse reads assets used by the presentation
+// from the file system using ioutil.ReadFile.
+func Parse(r io.Reader, name string, mode ParseMode) (*Doc, error) {
+	ctx := Context{ReadFile: ioutil.ReadFile}
+	return ctx.Parse(r, name, mode)
+}
+
 // isHeading matches any section heading.
 var isHeading = regexp.MustCompile(`^\*+ `)
 
@@ -256,7 +269,7 @@
 
 // parseSections parses Sections from lines for the section level indicated by
 // number (a nil number indicates the top level).
-func parseSections(name string, lines *Lines, number []int, doc *Doc) ([]Section, error) {
+func parseSections(ctx *Context, name string, lines *Lines, number []int, doc *Doc) ([]Section, error) {
 	var sections []Section
 	for i := 1; ; i++ {
 		// Next non-empty line is title.
@@ -312,7 +325,7 @@
 				e = List{Bullet: b}
 			case strings.HasPrefix(text, prefix+"* "):
 				lines.back()
-				subsecs, err := parseSections(name, lines, section.Number, doc)
+				subsecs, err := parseSections(ctx, name, lines, section.Number, doc)
 				if err != nil {
 					return nil, err
 				}
@@ -325,7 +338,7 @@
 				if parser == nil {
 					return nil, fmt.Errorf("%s:%d: unknown command %q\n", name, lines.line, text)
 				}
-				t, err := parser(name, lines.line, text)
+				t, err := parser(ctx, name, lines.line, text)
 				if err != nil {
 					return nil, err
 				}