diff --git a/present/args.go b/present/args.go new file mode 100644 index 0000000000..49ee1a98a7 --- /dev/null +++ b/present/args.go @@ -0,0 +1,229 @@ +// Copyright 2012 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. + +package present + +import ( + "errors" + "regexp" + "strconv" + "unicode/utf8" +) + +// This file is stolen from go/src/cmd/godoc/codewalk.go. +// It's an evaluator for the file address syntax implemented by acme and sam, +// but using Go-native regular expressions. +// To keep things reasonably close, this version uses (?m:re) for all user-provided +// regular expressions. That is the only change to the code from codewalk.go. +// See http://plan9.bell-labs.com/sys/doc/sam/sam.html Table II +// for details on the syntax. + +// addrToByte evaluates the given address starting at offset start in data. +// It returns the lo and hi byte offset of the matched region within data. +func addrToByteRange(addr string, start int, data []byte) (lo, hi int, err error) { + if addr == "" { + lo, hi = start, len(data) + return + } + var ( + dir byte + prevc byte + charOffset bool + ) + lo = start + hi = start + for addr != "" && err == nil { + c := addr[0] + switch c { + default: + err = errors.New("invalid address syntax near " + string(c)) + case ',': + if len(addr) == 1 { + hi = len(data) + } else { + _, hi, err = addrToByteRange(addr[1:], hi, data) + } + return + + case '+', '-': + if prevc == '+' || prevc == '-' { + lo, hi, err = addrNumber(data, lo, hi, prevc, 1, charOffset) + } + dir = c + + case '$': + lo = len(data) + hi = len(data) + if len(addr) > 1 { + dir = '+' + } + + case '#': + charOffset = true + + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + var i int + for i = 1; i < len(addr); i++ { + if addr[i] < '0' || addr[i] > '9' { + break + } + } + var n int + n, err = strconv.Atoi(addr[0:i]) + if err != nil { + break + } + lo, hi, err = addrNumber(data, lo, hi, dir, n, charOffset) + dir = 0 + charOffset = false + prevc = c + addr = addr[i:] + continue + + case '/': + var i, j int + Regexp: + for i = 1; i < len(addr); i++ { + switch addr[i] { + case '\\': + i++ + case '/': + j = i + 1 + break Regexp + } + } + if j == 0 { + j = i + } + pattern := addr[1:i] + lo, hi, err = addrRegexp(data, lo, hi, dir, pattern) + prevc = c + addr = addr[j:] + continue + } + prevc = c + addr = addr[1:] + } + + if err == nil && dir != 0 { + lo, hi, err = addrNumber(data, lo, hi, dir, 1, charOffset) + } + if err != nil { + return 0, 0, err + } + return lo, hi, nil +} + +// addrNumber applies the given dir, n, and charOffset to the address lo, hi. +// dir is '+' or '-', n is the count, and charOffset is true if the syntax +// used was #n. Applying +n (or +#n) means to advance n lines +// (or characters) after hi. Applying -n (or -#n) means to back up n lines +// (or characters) before lo. +// The return value is the new lo, hi. +func addrNumber(data []byte, lo, hi int, dir byte, n int, charOffset bool) (int, int, error) { + switch dir { + case 0: + lo = 0 + hi = 0 + fallthrough + + case '+': + if charOffset { + pos := hi + for ; n > 0 && pos < len(data); n-- { + _, size := utf8.DecodeRune(data[pos:]) + pos += size + } + if n == 0 { + return pos, pos, nil + } + break + } + // find next beginning of line + if hi > 0 { + for hi < len(data) && data[hi-1] != '\n' { + hi++ + } + } + lo = hi + if n == 0 { + return lo, hi, nil + } + for ; hi < len(data); hi++ { + if data[hi] != '\n' { + continue + } + switch n--; n { + case 1: + lo = hi + 1 + case 0: + return lo, hi + 1, nil + } + } + + case '-': + if charOffset { + // Scan backward for bytes that are not UTF-8 continuation bytes. + pos := lo + for ; pos > 0 && n > 0; pos-- { + if data[pos]&0xc0 != 0x80 { + n-- + } + } + if n == 0 { + return pos, pos, nil + } + break + } + // find earlier beginning of line + for lo > 0 && data[lo-1] != '\n' { + lo-- + } + hi = lo + if n == 0 { + return lo, hi, nil + } + for ; lo >= 0; lo-- { + if lo > 0 && data[lo-1] != '\n' { + continue + } + switch n--; n { + case 1: + hi = lo + case 0: + return lo, hi, nil + } + } + } + + return 0, 0, errors.New("address out of range") +} + +// addrRegexp searches for pattern in the given direction starting at lo, hi. +// The direction dir is '+' (search forward from hi) or '-' (search backward from lo). +// Backward searches are unimplemented. +func addrRegexp(data []byte, lo, hi int, dir byte, pattern string) (int, int, error) { + // We want ^ and $ to work as in sam/acme, so use ?m. + re, err := regexp.Compile("(?m:" + pattern + ")") + if err != nil { + return 0, 0, err + } + if dir == '-' { + // Could implement reverse search using binary search + // through file, but that seems like overkill. + return 0, 0, errors.New("reverse search not implemented") + } + m := re.FindIndex(data[hi:]) + if len(m) > 0 { + m[0] += hi + m[1] += hi + } else if hi > 0 { + // No match. Wrap to beginning of data. + m = re.FindIndex(data) + } + if len(m) == 0 { + return 0, 0, errors.New("no match for " + pattern) + } + return m[0], m[1], nil +} diff --git a/present/code.go b/present/code.go new file mode 100644 index 0000000000..4d82d9aed5 --- /dev/null +++ b/present/code.go @@ -0,0 +1,252 @@ +// Copyright 2012 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. + +package present + +import ( + "bufio" + "bytes" + "fmt" + "html/template" + "path/filepath" + "regexp" + "strconv" + "strings" +) + +// Is the playground available? +var PlayEnabled = false + +// TOOD(adg): replace the PlayEnabled flag with something less spaghetti-like. +// Instead this will probably be determined by a template execution Context +// value that contains various global metadata required when rendering +// templates. + +func init() { + Register("code", parseCode) + Register("play", parseCode) +} + +type Code struct { + Text template.HTML + Play bool // runnable code +} + +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. +// We pick off the HL first, for easy parsing. +var ( + highlightRE = regexp.MustCompile(`\s+HL([a-zA-Z0-9_]+)?$`) + hlCommentRE = regexp.MustCompile(`(.+) // HL(.*)$`) + codeRE = regexp.MustCompile(`\.(code|play)\s+([^\s]+)(\s+)?(.*)?$`) +) + +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. + highlight := "" + if hl := highlightRE.FindStringSubmatchIndex(cmd); len(hl) == 4 { + highlight = cmd[hl[2]:hl[3]] + cmd = cmd[:hl[2]-2] + } + + // Parse the remaining command line. + // 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", sourceFile, sourceLine) + } + command, file, addr := args[1], args[2], strings.TrimSpace(args[4]) + play := command == "play" && PlayEnabled + + // Read in code file and (optionally) match address. + filename := filepath.Join(filepath.Dir(sourceFile), file) + textBytes, err := ctx.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err) + } + lo, hi, err := addrToByteRange(addr, 0, textBytes) + if err != nil { + return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err) + } + + // Acme pattern matches can stop mid-line, + // so run to end of line in both directions if not at line start/end. + for lo > 0 && textBytes[lo-1] != '\n' { + lo-- + } + if hi > 0 { + for hi < len(textBytes) && textBytes[hi-1] != '\n' { + hi++ + } + } + + lines := codeLines(textBytes, lo, hi) + + for i, line := range lines { + // Replace tabs by spaces, which work better in HTML. + line.L = strings.Replace(line.L, "\t", " ", -1) + + // Highlight lines that end with "// HL[highlight]" + // and strip the magic comment. + if m := hlCommentRE.FindStringSubmatch(line.L); m != nil { + line.L = m[1] + line.HL = m[2] == highlight + } + + lines[i] = line + } + + data := &codeTemplateData{Lines: lines} + + // Include before and after in a hidden span for playground code. + if play { + data.Prefix = textBytes[:lo] + data.Suffix = textBytes[hi:] + } + + var buf bytes.Buffer + if err := codeTemplate.Execute(&buf, data); err != nil { + return nil, err + } + return Code{Text: template.HTML(buf.String()), Play: play}, nil +} + +type codeTemplateData struct { + Lines []codeLine + Prefix, Suffix []byte +} + +var leadingSpaceRE = regexp.MustCompile(`^[ \t]*`) + +var codeTemplate = template.Must(template.New("code").Funcs(template.FuncMap{ + "trimSpace": strings.TrimSpace, + "leadingSpace": leadingSpaceRE.FindString, +}).Parse(codeTemplateHTML)) + +const codeTemplateHTML = ` +{{with .Prefix}}
{{printf "%s" .}}
{{end}} + +
{{range .Lines}}{{/*
+	*/}}{{if .HL}}{{leadingSpace .L}}{{trimSpace .L}}{{/*
+	*/}}{{else}}{{.L}}{{end}}{{/*
+*/}}
+{{end}}
+ +{{with .Suffix}}
{{printf "%s" .}}
{{end}} +` + +// codeLine represents a line of code extracted from a source file. +type codeLine struct { + L string // The line of code. + N int // The line number from the source file. + HL bool // Whether the line should be highlighted. +} + +// codeLines takes a source file and returns the lines that +// span the byte range specified by start and end. +// It discards lines that end in "OMIT". +func codeLines(src []byte, start, end int) (lines []codeLine) { + startLine := 1 + for i, b := range src { + if i == start { + break + } + if b == '\n' { + startLine++ + } + } + s := bufio.NewScanner(bytes.NewReader(src[start:end])) + for n := startLine; s.Scan(); n++ { + l := s.Text() + if strings.HasSuffix(l, "OMIT") { + continue + } + lines = append(lines, codeLine{L: l, N: n}) + } + // Trim leading and trailing blank lines. + for len(lines) > 0 && len(lines[0].L) == 0 { + lines = lines[1:] + } + for len(lines) > 0 && len(lines[len(lines)-1].L) == 0 { + lines = lines[:len(lines)-1] + } + return +} + +func parseArgs(name string, line int, args []string) (res []interface{}, err error) { + res = make([]interface{}, len(args)) + for i, v := range args { + if len(v) == 0 { + return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v) + } + switch v[0] { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + n, err := strconv.Atoi(v) + if err != nil { + return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v) + } + res[i] = n + case '/': + if len(v) < 2 || v[len(v)-1] != '/' { + return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v) + } + res[i] = v + case '$': + res[i] = "$" + default: + return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v) + } + } + return +} + +// parseArg returns the integer or string value of the argument and tells which it is. +func parseArg(arg interface{}, max int) (ival int, sval string, isInt bool, err error) { + switch n := arg.(type) { + case int: + if n <= 0 || n > max { + return 0, "", false, fmt.Errorf("%d is out of range", n) + } + return n, "", true, nil + case string: + return 0, n, false, nil + } + return 0, "", false, fmt.Errorf("unrecognized argument %v type %T", arg, arg) +} + +// match identifies the input line that matches the pattern in a code invocation. +// If start>0, match lines starting there rather than at the beginning. +// The return value is 1-indexed. +func match(file string, start int, lines []string, pattern string) (int, error) { + // $ matches the end of the file. + if pattern == "$" { + if len(lines) == 0 { + return 0, fmt.Errorf("%q: empty file", file) + } + return len(lines), nil + } + // /regexp/ matches the line that matches the regexp. + if len(pattern) > 2 && pattern[0] == '/' && pattern[len(pattern)-1] == '/' { + re, err := regexp.Compile(pattern[1 : len(pattern)-1]) + if err != nil { + return 0, err + } + for i := start; i < len(lines); i++ { + if re.MatchString(lines[i]) { + return i + 1, nil + } + } + return 0, fmt.Errorf("%s: no match for %#q", file, pattern) + } + return 0, fmt.Errorf("unrecognized pattern: %q", pattern) +} diff --git a/present/code.go.orig b/present/code.go.orig new file mode 100644 index 0000000000..ab4d888d8f --- /dev/null +++ b/present/code.go.orig @@ -0,0 +1,252 @@ +// Copyright 2012 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. + +package present + +import ( + "bufio" + "bytes" + "fmt" + "html/template" + "path/filepath" + "regexp" + "strconv" + "strings" +) + +// Is the playground available? +var PlayEnabled = false + +// TOOD(adg): replace the PlayEnabled flag with something less spaghetti-like. +// Instead this will probably be determined by a template execution Context +// value that contains various global metadata required when rendering +// templates. + +func init() { + Register("code", parseCode) + Register("play", parseCode) +} + +type Code struct { + Text template.HTML + Play bool // runnable code +} + +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. +// We pick off the HL first, for easy parsing. +var ( + highlightRE = regexp.MustCompile(`\s+HL([a-zA-Z0-9_]+)?$`) + hlCommentRE = regexp.MustCompile(`(.+) // HL(.*)$`) + codeRE = regexp.MustCompile(`\.(code|play)\s+([^\s]+)(\s+)?(.*)?$`) +) + +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. + highlight := "" + if hl := highlightRE.FindStringSubmatchIndex(cmd); len(hl) == 4 { + highlight = cmd[hl[2]:hl[3]] + cmd = cmd[:hl[2]-2] + } + + // Parse the remaining command line. + // 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", sourceFile, sourceLine) + } + command, file, addr := args[1], args[2], strings.TrimSpace(args[4]) + play := command == "play" && PlayEnabled + + // Read in code file and (optionally) match address. + filename := filepath.Join(filepath.Dir(sourceFile), file) + textBytes, err := ctx.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err) + } + lo, hi, err := addrToByteRange(addr, 0, textBytes) + if err != nil { + return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err) + } + + // Acme pattern matches can stop mid-line, + // so run to end of line in both directions if not at line start/end. + for lo > 0 && textBytes[lo-1] != '\n' { + lo-- + } + if hi > 0 { + for hi < len(textBytes) && textBytes[hi-1] != '\n' { + hi++ + } + } + + lines := codeLines(textBytes, lo, hi) + + for i, line := range lines { + // Replace tabs by spaces, which work better in HTML. + line.L = strings.Replace(line.L, "\t", " ", -1) + + // Highlight lines that end with "// HL[highlight]" + // and strip the magic comment. + if m := hlCommentRE.FindStringSubmatch(line.L); m != nil { + line.L = m[1] + line.HL = m[2] == highlight + } + + lines[i] = line + } + + data := &codeTemplateData{Lines: lines} + + // Include before and after in a hidden span for playground code. + if play { + data.Prefix = textBytes[:lo] + data.Suffix = textBytes[hi:] + } + + var buf bytes.Buffer + if err := codeTemplate.Execute(&buf, data); err != nil { + return nil, err + } + return Code{Text: template.HTML(buf.String()), Play: play}, nil +} + +type codeTemplateData struct { + Lines []codeLine + Prefix, Suffix []byte +} + +var leadingSpaceRE = regexp.MustCompile(`^[ \t]*`) + +var codeTemplate = template.Must(template.New("code").Funcs(template.FuncMap{ + "trimSpace": strings.TrimSpace, + "leadingSpace": leadingSpaceRE.FindString, +}).Parse(codeTemplateHTML)) + +const codeTemplateHTML = ` +{{with .Prefix}}
{{printf "%s" .}}
{{end}} + +
{{range .Lines}}{{/*
+	*/}}{{if .HL}}{{leadingSpace .L}}{{trimSpace .L}}{{/*
+	*/}}{{else}}{{.L}}{{end}}{{/*
+*/}}
+{{end}}
+ +{{with .Suffix}}
{{printf "%s" .}}
{{end}} +` + +// codeLine represents a line of code extracted from a source file. +type codeLine struct { + L string // The line of code. + N int // The line number from the source file. + HL bool // Whether the line should be highlighted. +} + +// codeLines takes a source file and returns the lines that +// span the byte range specified by start and end. +// It discards lines that end in "OMIT". +func codeLines(src []byte, start, end int) (lines []codeLine) { + startLine := 1 + for i, b := range src { + if i == start { + break + } + if b == '\n' { + startLine++ + } + } + s := bufio.NewScanner(bytes.NewReader(src[start:end])) + for n := startLine; s.Scan(); n++ { + l := s.Text() + if strings.HasSuffix(l, "OMIT") { + continue + } + lines = append(lines, codeLine{L: l, N: n}) + } + // Trim leading and trailing blank lines. + for len(lines) > 0 && len(lines[0].L) == 0 { + lines = lines[1:] + } + for len(lines) > 0 && len(lines[len(lines)-1].L) == 0 { + lines = lines[:len(lines)-1] + } + return +} + +func parseArgs(name string, line int, args []string) (res []interface{}, err error) { + res = make([]interface{}, len(args)) + for i, v := range args { + if len(v) == 0 { + return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v) + } + switch v[0] { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + n, err := strconv.Atoi(v) + if err != nil { + return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v) + } + res[i] = n + case '/': + if len(v) < 2 || v[len(v)-1] != '/' { + return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v) + } + res[i] = v + case '$': + res[i] = "$" + default: + return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v) + } + } + return +} + +// parseArg returns the integer or string value of the argument and tells which it is. +func parseArg(arg interface{}, max int) (ival int, sval string, isInt bool, err error) { + switch n := arg.(type) { + case int: + if n <= 0 || n > max { + return 0, "", false, fmt.Errorf("%d is out of range", n) + } + return n, "", true, nil + case string: + return 0, n, false, nil + } + return 0, "", false, fmt.Errorf("unrecognized argument %v type %T", arg, arg) +} + +// match identifies the input line that matches the pattern in a code invocation. +// If start>0, match lines starting there rather than at the beginning. +// The return value is 1-indexed. +func match(file string, start int, lines []string, pattern string) (int, error) { + // $ matches the end of the file. + if pattern == "$" { + if len(lines) == 0 { + return 0, fmt.Errorf("%q: empty file", file) + } + return len(lines), nil + } + // /regexp/ matches the line that matches the regexp. + if len(pattern) > 2 && pattern[0] == '/' && pattern[len(pattern)-1] == '/' { + re, err := regexp.Compile(pattern[1 : len(pattern)-1]) + if err != nil { + return 0, err + } + for i := start; i < len(lines); i++ { + if re.MatchString(lines[i]) { + return i + 1, nil + } + } + return 0, fmt.Errorf("%s: no match for %#q", file, pattern) + } + return 0, fmt.Errorf("unrecognized pattern: %q", pattern) +} diff --git a/present/doc.go b/present/doc.go new file mode 100644 index 0000000000..32be15081a --- /dev/null +++ b/present/doc.go @@ -0,0 +1,184 @@ +// Copyright 2011 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. + +/* +The present file format + +Present files have the following format. The first non-blank non-comment +line is the title, so the header looks like + + Title of document + Subtitle of document + 15:04 2 Jan 2006 + Tags: foo, bar, baz + + Author Name + Job title, Company + joe@example.com + http://url/ + @twitter_name + +The subtitle, date, and tags lines are optional. + +The date line may be written without a time: + 2 Jan 2006 +In this case, the time will be interpreted as 10am UTC on that date. + +The tags line is a comma-separated list of tags that may be used to categorize +the document. + +The author section may contain a mixture of text, twitter names, and links. +For slide presentations, only the plain text lines will be displayed on the +first slide. + +Multiple presenters may be specified, separated by a blank line. + +After that come slides/sections, each after a blank line: + + * Title of slide or section (must have asterisk) + + Some Text + + ** Subsection + + - bullets + - more bullets + - a bullet with + + *** Sub-subsection + + Some More text + + Preformatted text + is indented (however you like) + + Further Text, including invocations like: + + .code x.go /^func main/,/^}/ + .play y.go + .image image.jpg + .iframe http://foo + .link http://foo label + .html file.html + + Again, more text + +Blank lines are OK (not mandatory) after the title and after the +text. Text, bullets, and .code etc. are all optional; title is +not. + +Lines starting with # in column 1 are commentary. + +Fonts: + +Within the input for plain text or lists, text bracketed by font +markers will be presented in italic, bold, or program font. +Marker characters are _ (italic), * (bold) and ` (program font). +Unmatched markers appear as plain text. +Within marked text, a single marker character becomes a space +and a doubled single marker quotes the marker character. + + _italic_ + *bold* + `program` + _this_is_all_italic_ + _Why_use_scoped__ptr_? Use plain ***ptr* instead. + +Inline links: + +Links can be included in any text with the form [[url][label]], or +[[url]] to use the URL itself as the label. + +Functions: + +A number of template functions are available through invocations +in the input text. Each such invocation contains a period as the +first character on the line, followed immediately by the name of +the function, followed by any arguments. A typical invocation might +be + .play demo.go /^func show/,/^}/ +(except that the ".play" must be at the beginning of the line and +not be indented like this.) + +Here follows a description of the functions: + +code: + +Injects program source into the output by extracting code from files +and injecting them as HTML-escaped
 blocks.  The argument is
+a file name followed by an optional address that specifies what
+section of the file to display. The address syntax is similar in
+its simplest form to that of ed, but comes from sam and is more
+general. See
+	http://plan9.bell-labs.com/sys/doc/sam/sam.html Table II
+for full details. The displayed block is always rounded out to a
+full line at both ends.
+
+If no pattern is present, the entire file is displayed.
+
+Any line in the program that ends with the four characters
+	OMIT
+is deleted from the source before inclusion, making it easy
+to write things like
+	.code test.go /START OMIT/,/END OMIT/
+to find snippets like this
+	tedious_code = boring_function()
+	// START OMIT
+	interesting_code = fascinating_function()
+	// END OMIT
+and see only this:
+	interesting_code = fascinating_function()
+
+Also, inside the displayed text a line that ends
+	// HL
+will be highlighted in the display; the 'h' key in the browser will
+toggle extra emphasis of any highlighted lines. A highlighting mark
+may have a suffix word, such as
+	// HLxxx
+Such highlights are enabled only if the code invocation ends with
+"HL" followed by the word:
+	.code test.go /^type Foo/,/^}/ HLxxx
+
+play:
+
+The function "play" is the same as "code" but puts a button
+on the displayed source so the program can be run from the browser.
+Although only the selected text is shown, all the source is included
+in the HTML output so it can be presented to the compiler.
+
+link:
+
+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:
+
+The template uses the function "image" to inject picture files.
+
+The syntax is simple: 1 or 3 space-separated arguments.
+The first argument is always the file name.
+If there are more arguments, they are the height and width;
+both must be present.
+
+	.image images/betsy.jpg 100 200
+
+iframe:
+
+The function "iframe" injects iframes (pages inside pages).
+Its syntax is the same as that of image.
+
+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.
+It is your responsibilty to make sure the included HTML is valid and safe.
+
+	.html file.html
+
+*/
+package present
diff --git a/present/html.go b/present/html.go
new file mode 100644
index 0000000000..cca90ef4af
--- /dev/null
+++ b/present/html.go
@@ -0,0 +1,31 @@
+package present
+
+import (
+	"errors"
+	"html/template"
+	"path/filepath"
+	"strings"
+)
+
+func init() {
+	Register("html", parseHTML)
+}
+
+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 := ctx.ReadFile(name)
+	if err != nil {
+		return nil, err
+	}
+	return HTML{template.HTML(b)}, nil
+}
+
+type HTML struct {
+	template.HTML
+}
+
+func (s HTML) TemplateName() string { return "html" }
diff --git a/present/iframe.go b/present/iframe.go
new file mode 100644
index 0000000000..2f3c5e55a8
--- /dev/null
+++ b/present/iframe.go
@@ -0,0 +1,45 @@
+// Copyright 2013 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.
+
+package present
+
+import (
+	"fmt"
+	"strings"
+)
+
+func init() {
+	Register("iframe", parseIframe)
+}
+
+type Iframe struct {
+	URL    string
+	Width  int
+	Height int
+}
+
+func (i Iframe) TemplateName() string { return "iframe" }
+
+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:])
+	if err != nil {
+		return nil, err
+	}
+	switch len(a) {
+	case 0:
+		// no size parameters
+	case 2:
+		if v, ok := a[0].(int); ok {
+			i.Height = v
+		}
+		if v, ok := a[1].(int); ok {
+			i.Width = v
+		}
+	default:
+		return nil, fmt.Errorf("incorrect image invocation: %q", text)
+	}
+	return i, nil
+}
diff --git a/present/image.go b/present/image.go
new file mode 100644
index 0000000000..2bab429c7d
--- /dev/null
+++ b/present/image.go
@@ -0,0 +1,45 @@
+// Copyright 2012 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.
+
+package present
+
+import (
+	"fmt"
+	"strings"
+)
+
+func init() {
+	Register("image", parseImage)
+}
+
+type Image struct {
+	URL    string
+	Width  int
+	Height int
+}
+
+func (i Image) TemplateName() string { return "image" }
+
+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:])
+	if err != nil {
+		return nil, err
+	}
+	switch len(a) {
+	case 0:
+		// no size parameters
+	case 2:
+		if v, ok := a[0].(int); ok {
+			img.Height = v
+		}
+		if v, ok := a[1].(int); ok {
+			img.Width = v
+		}
+	default:
+		return nil, fmt.Errorf("incorrect image invocation: %q", text)
+	}
+	return img, nil
+}
diff --git a/present/link.go b/present/link.go
new file mode 100644
index 0000000000..a840b23dec
--- /dev/null
+++ b/present/link.go
@@ -0,0 +1,88 @@
+// Copyright 2012 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.
+
+package present
+
+import (
+	"fmt"
+	"net/url"
+	"strings"
+)
+
+func init() {
+	Register("link", parseLink)
+}
+
+type Link struct {
+	URL   *url.URL
+	Label string
+}
+
+func (l Link) TemplateName() string { return "link" }
+
+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 {
+		return nil, err
+	}
+	label := ""
+	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)
+	}
+	return Link{url, label}, nil
+}
+
+func renderLink(url, text string) string {
+	text = font(text)
+	if text == "" {
+		text = url
+	}
+	return fmt.Sprintf(`%s`, url, text)
+}
+
+// parseInlineLink parses an inline link at the start of s, and returns
+// a rendered HTML link and the total length of the raw inline link.
+// If no inline link is present, it returns all zeroes.
+func parseInlineLink(s string) (link string, length int) {
+	if !strings.HasPrefix(s, "[[") {
+		return
+	}
+	end := strings.Index(s, "]]")
+	if end == -1 {
+		return
+	}
+	urlEnd := strings.Index(s, "]")
+	rawURL := s[2:urlEnd]
+	const badURLChars = `<>"{}|\^[] ` + "`" // per RFC2396 section 2.4.3
+	if strings.ContainsAny(rawURL, badURLChars) {
+		return
+	}
+	if urlEnd == end {
+		simpleUrl := ""
+		url, err := url.Parse(rawURL)
+		if err == nil {
+			// If the URL is http://foo.com, drop the http://
+			// In other words, render [[http://golang.org]] as:
+			//   golang.org
+			if strings.HasPrefix(rawURL, url.Scheme+"://") {
+				simpleUrl = strings.TrimPrefix(rawURL, url.Scheme+"://")
+			} else if strings.HasPrefix(rawURL, url.Scheme+":") {
+				simpleUrl = strings.TrimPrefix(rawURL, url.Scheme+":")
+			}
+		}
+		return renderLink(rawURL, simpleUrl), end + 2
+	}
+	if s[urlEnd:urlEnd+2] != "][" {
+		return
+	}
+	text := s[urlEnd+2 : end]
+	return renderLink(rawURL, text), end + 2
+}
diff --git a/present/link_test.go b/present/link_test.go
new file mode 100644
index 0000000000..334e72bdcc
--- /dev/null
+++ b/present/link_test.go
@@ -0,0 +1,40 @@
+// Copyright 2012 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.
+
+package present
+
+import "testing"
+
+func TestInlineParsing(t *testing.T) {
+	var tests = []struct {
+		in     string
+		link   string
+		text   string
+		length int
+	}{
+		{"[[http://golang.org]]", "http://golang.org", "golang.org", 21},
+		{"[[http://golang.org][]]", "http://golang.org", "http://golang.org", 23},
+		{"[[http://golang.org]] this is ignored", "http://golang.org", "golang.org", 21},
+		{"[[http://golang.org][link]]", "http://golang.org", "link", 27},
+		{"[[http://golang.org][two words]]", "http://golang.org", "two words", 32},
+		{"[[http://golang.org][*link*]]", "http://golang.org", "link", 29},
+		{"[[http://bad[url]]", "", "", 0},
+		{"[[http://golang.org][a [[link]] ]]", "http://golang.org", "a [[link", 31},
+		{"[[http:// *spaces* .com]]", "", "", 0},
+		{"[[http://bad`char.com]]", "", "", 0},
+		{" [[http://google.com]]", "", "", 0},
+		{"[[mailto:gopher@golang.org][Gopher]]", "mailto:gopher@golang.org", "Gopher", 36},
+		{"[[mailto:gopher@golang.org]]", "mailto:gopher@golang.org", "gopher@golang.org", 28},
+	}
+
+	for i, test := range tests {
+		link, length := parseInlineLink(test.in)
+		if length == 0 && test.length == 0 {
+			continue
+		}
+		if a := renderLink(test.link, test.text); length != test.length || link != a {
+			t.Errorf("#%d: parseInlineLink(%q):\ngot\t%q, %d\nwant\t%q, %d", i, test.in, link, length, a, test.length)
+		}
+	}
+}
diff --git a/present/parse.go b/present/parse.go
new file mode 100644
index 0000000000..a78a85b6ac
--- /dev/null
+++ b/present/parse.go
@@ -0,0 +1,495 @@
+// Copyright 2011 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.
+
+package present
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"fmt"
+	"html/template"
+	"io"
+	"io/ioutil"
+	"log"
+	"net/url"
+	"regexp"
+	"strings"
+	"time"
+	"unicode"
+	"unicode/utf8"
+)
+
+var (
+	parsers = make(map[string]ParseFunc)
+	funcs   = template.FuncMap{}
+)
+
+// Template returns an empty template with the action functions in its FuncMap.
+func Template() *template.Template {
+	return template.New("").Funcs(funcs)
+}
+
+// Render renders the doc to the given writer using the provided template.
+func (d *Doc) Render(w io.Writer, t *template.Template) error {
+	data := struct {
+		*Doc
+		Template    *template.Template
+		PlayEnabled bool
+	}{d, t, PlayEnabled}
+	return t.ExecuteTemplate(w, "root", data)
+}
+
+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
+// present input text.
+func Register(name string, parser ParseFunc) {
+	if len(name) == 0 || name[0] == ';' {
+		panic("bad name in Register: " + name)
+	}
+	parsers["."+name] = parser
+}
+
+// Doc represents an entire document.
+type Doc struct {
+	Title    string
+	Subtitle string
+	Time     time.Time
+	Authors  []Author
+	Sections []Section
+	Tags     []string
+}
+
+// Author represents the person who wrote and/or is presenting the document.
+type Author struct {
+	Elem []Elem
+}
+
+// TextElem returns the first text elements of the author details.
+// This is used to display the author' name, job title, and company
+// without the contact details.
+func (p *Author) TextElem() (elems []Elem) {
+	for _, el := range p.Elem {
+		if _, ok := el.(Text); !ok {
+			break
+		}
+		elems = append(elems, el)
+	}
+	return
+}
+
+// Section represents a section of a document (such as a presentation slide)
+// comprising a title and a list of elements.
+type Section struct {
+	Number []int
+	Title  string
+	Elem   []Elem
+}
+
+func (s Section) Sections() (sections []Section) {
+	for _, e := range s.Elem {
+		if section, ok := e.(Section); ok {
+			sections = append(sections, section)
+		}
+	}
+	return
+}
+
+// Level returns the level of the given section.
+// The document title is level 1, main section 2, etc.
+func (s Section) Level() int {
+	return len(s.Number) + 1
+}
+
+// FormattedNumber returns a string containing the concatenation of the
+// numbers identifying a Section.
+func (s Section) FormattedNumber() string {
+	b := &bytes.Buffer{}
+	for _, n := range s.Number {
+		fmt.Fprintf(b, "%v.", n)
+	}
+	return b.String()
+}
+
+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
+}
+
+// renderElem implements the elem template function, used to render
+// sub-templates.
+func renderElem(t *template.Template, e Elem) (template.HTML, error) {
+	var data interface{} = e
+	if s, ok := e.(Section); ok {
+		data = struct {
+			Section
+			Template *template.Template
+		}{s, t}
+	}
+	return execTemplate(t, e.TemplateName(), data)
+}
+
+func init() {
+	funcs["elem"] = renderElem
+}
+
+// execTemplate is a helper to execute a template and return the output as a
+// template.HTML value.
+func execTemplate(t *template.Template, name string, data interface{}) (template.HTML, error) {
+	b := new(bytes.Buffer)
+	err := t.ExecuteTemplate(b, name, data)
+	if err != nil {
+		return "", err
+	}
+	return template.HTML(b.String()), nil
+}
+
+// Text represents an optionally preformatted paragraph.
+type Text struct {
+	Lines []string
+	Pre   bool
+}
+
+func (t Text) TemplateName() string { return "text" }
+
+// List represents a bulleted list.
+type List struct {
+	Bullet []string
+}
+
+func (l List) TemplateName() string { return "list" }
+
+// Lines is a helper for parsing line-based input.
+type Lines struct {
+	line int // 0 indexed, so has 1-indexed number of last line returned
+	text []string
+}
+
+func readLines(r io.Reader) (*Lines, error) {
+	var lines []string
+	s := bufio.NewScanner(r)
+	for s.Scan() {
+		lines = append(lines, s.Text())
+	}
+	if err := s.Err(); err != nil {
+		return nil, err
+	}
+	return &Lines{0, lines}, nil
+}
+
+func (l *Lines) next() (text string, ok bool) {
+	for {
+		current := l.line
+		l.line++
+		if current >= len(l.text) {
+			return "", false
+		}
+		text = l.text[current]
+		// Lines starting with # are comments.
+		if len(text) == 0 || text[0] != '#' {
+			ok = true
+			break
+		}
+	}
+	return
+}
+
+func (l *Lines) back() {
+	l.line--
+}
+
+func (l *Lines) nextNonEmpty() (text string, ok bool) {
+	for {
+		text, ok = l.next()
+		if !ok {
+			return
+		}
+		if len(text) > 0 {
+			break
+		}
+	}
+	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
+
+const (
+	// If set, parse only the title and subtitle.
+	TitlesOnly ParseMode = 1
+)
+
+// 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 {
+		return nil, err
+	}
+	err = parseHeader(doc, lines)
+	if err != nil {
+		return nil, err
+	}
+	if mode&TitlesOnly != 0 {
+		return doc, nil
+	}
+	// Authors
+	if doc.Authors, err = parseAuthors(lines); err != nil {
+		return nil, err
+	}
+	// Sections
+	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(`^\*+ `)
+
+// lesserHeading returns true if text is a heading of a lesser or equal level
+// than that denoted by prefix.
+func lesserHeading(text, prefix string) bool {
+	return isHeading.MatchString(text) && !strings.HasPrefix(text, prefix+"*")
+}
+
+// parseSections parses Sections from lines for the section level indicated by
+// number (a nil number indicates the top level).
+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.
+		text, ok := lines.nextNonEmpty()
+		for ok && text == "" {
+			text, ok = lines.next()
+		}
+		if !ok {
+			break
+		}
+		prefix := strings.Repeat("*", len(number)+1)
+		if !strings.HasPrefix(text, prefix+" ") {
+			lines.back()
+			break
+		}
+		section := Section{
+			Number: append(append([]int{}, number...), i),
+			Title:  text[len(prefix)+1:],
+		}
+		text, ok = lines.nextNonEmpty()
+		for ok && !lesserHeading(text, prefix) {
+			var e Elem
+			r, _ := utf8.DecodeRuneInString(text)
+			switch {
+			case unicode.IsSpace(r):
+				i := strings.IndexFunc(text, func(r rune) bool {
+					return !unicode.IsSpace(r)
+				})
+				if i < 0 {
+					break
+				}
+				indent := text[:i]
+				var s []string
+				for ok && (strings.HasPrefix(text, indent) || text == "") {
+					if text != "" {
+						text = text[i:]
+					}
+					s = append(s, text)
+					text, ok = lines.next()
+				}
+				lines.back()
+				pre := strings.Join(s, "\n")
+				pre = strings.Replace(pre, "\t", "    ", -1) // browsers treat tabs badly
+				pre = strings.TrimRightFunc(pre, unicode.IsSpace)
+				e = Text{Lines: []string{pre}, Pre: true}
+			case strings.HasPrefix(text, "- "):
+				var b []string
+				for ok && strings.HasPrefix(text, "- ") {
+					b = append(b, text[2:])
+					text, ok = lines.next()
+				}
+				lines.back()
+				e = List{Bullet: b}
+			case strings.HasPrefix(text, prefix+"* "):
+				lines.back()
+				subsecs, err := parseSections(ctx, name, lines, section.Number, doc)
+				if err != nil {
+					return nil, err
+				}
+				for _, ss := range subsecs {
+					section.Elem = append(section.Elem, ss)
+				}
+			case strings.HasPrefix(text, "."):
+				args := strings.Fields(text)
+				parser := parsers[args[0]]
+				if parser == nil {
+					return nil, fmt.Errorf("%s:%d: unknown command %q\n", name, lines.line, text)
+				}
+				t, err := parser(ctx, name, lines.line, text)
+				if err != nil {
+					return nil, err
+				}
+				e = t
+			default:
+				var l []string
+				for ok && strings.TrimSpace(text) != "" {
+					if text[0] == '.' { // Command breaks text block.
+						break
+					}
+					if strings.HasPrefix(text, `\.`) { // Backslash escapes initial period.
+						text = text[1:]
+					}
+					l = append(l, text)
+					text, ok = lines.next()
+				}
+				if len(l) > 0 {
+					e = Text{Lines: l}
+				}
+			}
+			if e != nil {
+				section.Elem = append(section.Elem, e)
+			}
+			text, ok = lines.nextNonEmpty()
+		}
+		if isHeading.MatchString(text) {
+			lines.back()
+		}
+		sections = append(sections, section)
+	}
+	return sections, nil
+}
+
+func parseHeader(doc *Doc, lines *Lines) error {
+	var ok bool
+	// First non-empty line starts header.
+	doc.Title, ok = lines.nextNonEmpty()
+	if !ok {
+		return errors.New("unexpected EOF; expected title")
+	}
+	for {
+		text, ok := lines.next()
+		if !ok {
+			return errors.New("unexpected EOF")
+		}
+		if text == "" {
+			break
+		}
+		const tagPrefix = "Tags:"
+		if strings.HasPrefix(text, tagPrefix) {
+			tags := strings.Split(text[len(tagPrefix):], ",")
+			for i := range tags {
+				tags[i] = strings.TrimSpace(tags[i])
+			}
+			doc.Tags = append(doc.Tags, tags...)
+		} else if t, ok := parseTime(text); ok {
+			doc.Time = t
+		} else if doc.Subtitle == "" {
+			doc.Subtitle = text
+		} else {
+			return fmt.Errorf("unexpected header line: %q", text)
+		}
+	}
+	return nil
+}
+
+func parseAuthors(lines *Lines) (authors []Author, err error) {
+	// This grammar demarcates authors with blanks.
+
+	// Skip blank lines.
+	if _, ok := lines.nextNonEmpty(); !ok {
+		return nil, errors.New("unexpected EOF")
+	}
+	lines.back()
+
+	var a *Author
+	for {
+		text, ok := lines.next()
+		if !ok {
+			return nil, errors.New("unexpected EOF")
+		}
+
+		// If we find a section heading, we're done.
+		if strings.HasPrefix(text, "* ") {
+			lines.back()
+			break
+		}
+
+		// If we encounter a blank we're done with this author.
+		if a != nil && len(text) == 0 {
+			authors = append(authors, *a)
+			a = nil
+			continue
+		}
+		if a == nil {
+			a = new(Author)
+		}
+
+		// Parse the line. Those that
+		// - begin with @ are twitter names,
+		// - contain slashes are links, or
+		// - contain an @ symbol are an email address.
+		// The rest is just text.
+		var el Elem
+		switch {
+		case strings.HasPrefix(text, "@"):
+			el = parseURL("http://twitter.com/" + text[1:])
+		case strings.Contains(text, ":"):
+			el = parseURL(text)
+		case strings.Contains(text, "@"):
+			el = parseURL("mailto:" + text)
+		}
+		if l, ok := el.(Link); ok {
+			l.Label = text
+			el = l
+		}
+		if el == nil {
+			el = Text{Lines: []string{text}}
+		}
+		a.Elem = append(a.Elem, el)
+	}
+	if a != nil {
+		authors = append(authors, *a)
+	}
+	return authors, nil
+}
+
+func parseURL(text string) Elem {
+	u, err := url.Parse(text)
+	if err != nil {
+		log.Printf("Parse(%q): %v", text, err)
+		return nil
+	}
+	return Link{URL: u}
+}
+
+func parseTime(text string) (t time.Time, ok bool) {
+	t, err := time.Parse("15:04 2 Jan 2006", text)
+	if err == nil {
+		return t, true
+	}
+	t, err = time.Parse("2 Jan 2006", text)
+	if err == nil {
+		// at 11am UTC it is the same date everywhere
+		t = t.Add(time.Hour * 11)
+		return t, true
+	}
+	return time.Time{}, false
+}
diff --git a/present/style.go b/present/style.go
new file mode 100644
index 0000000000..1cd240de72
--- /dev/null
+++ b/present/style.go
@@ -0,0 +1,166 @@
+// Copyright 2012 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.
+
+package present
+
+import (
+	"bytes"
+	"html"
+	"html/template"
+	"strings"
+	"unicode"
+	"unicode/utf8"
+)
+
+/*
+	Fonts are demarcated by an initial and final char bracketing a
+	space-delimited word, plus possibly some terminal punctuation.
+	The chars are
+		_ for italic
+		* for bold
+		` (back quote) for fixed width.
+	Inner appearances of the char become spaces. For instance,
+		_this_is_italic_!
+	becomes
+		this is italic!
+*/
+
+func init() {
+	funcs["style"] = Style
+}
+
+// Style returns s with HTML entities escaped and font indicators turned into
+// HTML font tags.
+func Style(s string) template.HTML {
+	return template.HTML(font(html.EscapeString(s)))
+}
+
+// font returns s with font indicators turned into HTML font tags.
+func font(s string) string {
+	if strings.IndexAny(s, "[`_*") == -1 {
+		return s
+	}
+	words := split(s)
+	var b bytes.Buffer
+Word:
+	for w, word := range words {
+		if len(word) < 2 {
+			continue Word
+		}
+		if link, _ := parseInlineLink(word); link != "" {
+			words[w] = link
+			continue Word
+		}
+		const punctuation = `.,;:()!?—–'"`
+		const marker = "_*`"
+		// Initial punctuation is OK but must be peeled off.
+		first := strings.IndexAny(word, marker)
+		if first == -1 {
+			continue Word
+		}
+		// Is the marker prefixed only by punctuation?
+		for _, r := range word[:first] {
+			if !strings.ContainsRune(punctuation, r) {
+				continue Word
+			}
+		}
+		open, word := word[:first], word[first:]
+		char := word[0] // ASCII is OK.
+		close := ""
+		switch char {
+		default:
+			continue Word
+		case '_':
+			open += ""
+			close = ""
+		case '*':
+			open += ""
+			close = ""
+		case '`':
+			open += ""
+			close = ""
+		}
+		// Terminal punctuation is OK but must be peeled off.
+		last := strings.LastIndex(word, word[:1])
+		if last == 0 {
+			continue Word
+		}
+		head, tail := word[:last+1], word[last+1:]
+		for _, r := range tail {
+			if !strings.ContainsRune(punctuation, r) {
+				continue Word
+			}
+		}
+		b.Reset()
+		b.WriteString(open)
+		var wid int
+		for i := 1; i < len(head)-1; i += wid {
+			var r rune
+			r, wid = utf8.DecodeRuneInString(head[i:])
+			if r != rune(char) {
+				// Ordinary character.
+				b.WriteRune(r)
+				continue
+			}
+			if head[i+1] != char {
+				// Inner char becomes space.
+				b.WriteRune(' ')
+				continue
+			}
+			// Doubled char becomes real char.
+			// Not worth worrying about "_x__".
+			b.WriteByte(char)
+			wid++ // Consumed two chars, both ASCII.
+		}
+		b.WriteString(close) // Write closing tag.
+		b.WriteString(tail)  // Restore trailing punctuation.
+		words[w] = b.String()
+	}
+	return strings.Join(words, "")
+}
+
+// split is like strings.Fields but also returns the runs of spaces
+// and treats inline links as distinct words.
+func split(s string) []string {
+	var (
+		words = make([]string, 0, 10)
+		start = 0
+	)
+
+	// appendWord appends the string s[start:end] to the words slice.
+	// If the word contains the beginning of a link, the non-link portion
+	// of the word and the entire link are appended as separate words,
+	// and the start index is advanced to the end of the link.
+	appendWord := func(end int) {
+		if j := strings.Index(s[start:end], "[["); j > -1 {
+			if _, l := parseInlineLink(s[start+j:]); l > 0 {
+				// Append portion before link, if any.
+				if j > 0 {
+					words = append(words, s[start:start+j])
+				}
+				// Append link itself.
+				words = append(words, s[start+j:start+j+l])
+				// Advance start index to end of link.
+				start = start + j + l
+				return
+			}
+		}
+		// No link; just add the word.
+		words = append(words, s[start:end])
+		start = end
+	}
+
+	wasSpace := false
+	for i, r := range s {
+		isSpace := unicode.IsSpace(r)
+		if i > start && isSpace != wasSpace {
+			appendWord(i)
+		}
+		wasSpace = isSpace
+	}
+	for start < len(s) {
+		appendWord(len(s))
+	}
+	return words
+}
diff --git a/present/style_test.go b/present/style_test.go
new file mode 100644
index 0000000000..d04db72d25
--- /dev/null
+++ b/present/style_test.go
@@ -0,0 +1,116 @@
+// Copyright 2012 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.
+
+package present
+
+import (
+	"fmt"
+	"reflect"
+	"testing"
+)
+
+func TestSplit(t *testing.T) {
+	var tests = []struct {
+		in  string
+		out []string
+	}{
+		{"", []string{}},
+		{" ", []string{" "}},
+		{"abc", []string{"abc"}},
+		{"abc def", []string{"abc", " ", "def"}},
+		{"abc def ", []string{"abc", " ", "def", " "}},
+		{"hey [[http://golang.org][Gophers]] around",
+			[]string{"hey", " ", "[[http://golang.org][Gophers]]", " ", "around"}},
+		{"A [[http://golang.org/doc][two words]] link",
+			[]string{"A", " ", "[[http://golang.org/doc][two words]]", " ", "link"}},
+		{"Visit [[http://golang.org/doc]] now",
+			[]string{"Visit", " ", "[[http://golang.org/doc]]", " ", "now"}},
+		{"not [[http://golang.org/doc][a [[link]] ]] around",
+			[]string{"not", " ", "[[http://golang.org/doc][a [[link]]", " ", "]]", " ", "around"}},
+		{"[[http://golang.org][foo bar]]",
+			[]string{"[[http://golang.org][foo bar]]"}},
+		{"ends with [[http://golang.org][link]]",
+			[]string{"ends", " ", "with", " ", "[[http://golang.org][link]]"}},
+		{"my talk ([[http://talks.golang.org/][slides here]])",
+			[]string{"my", " ", "talk", " ", "(", "[[http://talks.golang.org/][slides here]]", ")"}},
+	}
+	for _, test := range tests {
+		out := split(test.in)
+		if !reflect.DeepEqual(out, test.out) {
+			t.Errorf("split(%q):\ngot\t%q\nwant\t%q", test.in, out, test.out)
+		}
+	}
+}
+
+func TestFont(t *testing.T) {
+	var tests = []struct {
+		in  string
+		out string
+	}{
+		{"", ""},
+		{" ", " "},
+		{"\tx", "\tx"},
+		{"_a_", "a"},
+		{"*a*", "a"},
+		{"`a`", "a"},
+		{"_a_b_", "a b"},
+		{"_a__b_", "a_b"},
+		{"_a___b_", "a_ b"},
+		{"*a**b*?", "a*b?"},
+		{"_a_<>_b_.", "a <> b."},
+		{"(_a_)", "(a)"},
+		{"((_a_), _b_, _c_).", "((a), b, c)."},
+		{"(_a)", "(_a)"},
+		{"(_a)", "(_a)"},
+		{"_Why_use_scoped__ptr_? Use plain ***ptr* instead.", "Why use scoped_ptr? Use plain *ptr instead."},
+		{"_hey_ [[http://golang.org][*Gophers*]] *around*",
+			`hey Gophers around`},
+		{"_hey_ [[http://golang.org][so _many_ *Gophers*]] *around*",
+			`hey so many Gophers around`},
+		{"Visit [[http://golang.org]] now",
+			`Visit golang.org now`},
+		{"my talk ([[http://talks.golang.org/][slides here]])",
+			`my talk (slides here)`},
+	}
+	for _, test := range tests {
+		out := font(test.in)
+		if out != test.out {
+			t.Errorf("font(%q):\ngot\t%q\nwant\t%q", test.in, out, test.out)
+		}
+	}
+}
+
+func TestStyle(t *testing.T) {
+	var tests = []struct {
+		in  string
+		out string
+	}{
+		{"", ""},
+		{" ", " "},
+		{"\tx", "\tx"},
+		{"_a_", "a"},
+		{"*a*", "a"},
+		{"`a`", "a"},
+		{"_a_b_", "a b"},
+		{"_a__b_", "a_b"},
+		{"_a___b_", "a_ b"},
+		{"*a**b*?", "a*b?"},
+		{"_a_<>_b_.", "a <> b."},
+		{"(_a_<>_b_)", "(a <> b)"},
+		{"((_a_), _b_, _c_).", "((a), b, c)."},
+		{"(_a)", "(_a)"},
+	}
+	for _, test := range tests {
+		out := string(Style(test.in))
+		if out != test.out {
+			t.Errorf("style(%q):\ngot\t%q\nwant\t%q", test.in, out, test.out)
+		}
+	}
+}
+
+func ExampleStyle() {
+	const s = "*Gophers* are _clearly_ > *cats*!"
+	fmt.Println(Style(s))
+	// Output: Gophers are clearly > cats!
+}