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}}
{{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 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}} {{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 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
+ 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! +}