diff --git a/present/args.go b/present/args.go new file mode 100644 index 00000000..49ee1a98 --- /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 00000000..4d82d9ae --- /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}}
{{end}} + +{{range .Lines}}{{/*
+ */}}{{if .HL}}{{leadingSpace .L}}{{trimSpace .L}}{{/*
+ */}}{{else}}{{.L}}{{end}}{{/*
+*/}}
+{{end}}
+
+{{with .Suffix}}{{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 00000000..ab4d888d
--- /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}}{{end}}
+
+{{range .Lines}}{{/*
+ */}}{{if .HL}}{{leadingSpace .L}}{{trimSpace .L}}{{/*
+ */}}{{else}}{{.L}}{{end}}{{/*
+*/}}
+{{end}}
+
+{{with .Suffix}}{{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 00000000..32be1508
--- /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
+ 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 00000000..cca90ef4
--- /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 00000000..2f3c5e55
--- /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 00000000..2bab429c
--- /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 00000000..a840b23d
--- /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 00000000..334e72bd
--- /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 00000000..a78a85b6
--- /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 00000000..1cd240de
--- /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 00000000..d04db72d
--- /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!
+}