1
0
mirror of https://github.com/golang/go synced 2024-11-18 04:14:49 -07:00

go.tools/godoc/present: move present package from go.talks

Godoc depends on go.talks/pkg/present by way of go.tools/pkg/blog.
Better to keep all godoc dependencies in one place.

R=golang-dev, dsymonds, r
CC=golang-dev
https://golang.org/cl/13656047
This commit is contained in:
Andrew Gerrand 2013-09-19 10:55:46 +10:00
parent a76da35c40
commit 9fc516408c
12 changed files with 1943 additions and 0 deletions

229
present/args.go Normal file
View File

@ -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
}

252
present/code.go Normal file
View File

@ -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}}<pre style="display: none"><span>{{printf "%s" .}}</span></pre>{{end}}
<pre>{{range .Lines}}<span num="{{.N}}">{{/*
*/}}{{if .HL}}{{leadingSpace .L}}<b>{{trimSpace .L}}</b>{{/*
*/}}{{else}}{{.L}}{{end}}{{/*
*/}}</span>
{{end}}</pre>
{{with .Suffix}}<pre style="display: none"><span>{{printf "%s" .}}</span></pre>{{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)
}

252
present/code.go.orig Normal file
View File

@ -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}}<pre style="display: none"><span>{{printf "%s" .}}</span></pre>{{end}}
<pre>{{range .Lines}}<span num="{{.N}}"{{if .HL}} class="hl"{{end}}>{{/*
*/}}{{if .HL}}{{leadingSpace .L}}<b>{{trimSpace .L}}</b>{{/*
*/}}{{else}}{{.L}}{{end}}{{/*
*/}}</span>
{{end}}</pre>
{{with .Suffix}}<pre style="display: none"><span>{{printf "%s" .}}</span></pre>{{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)
}

184
present/doc.go Normal file
View File

@ -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
<blank line>
Author Name
Job title, Company
joe@example.com
http://url/
@twitter_name
The subtitle, date, and tags lines are optional.
The date line may be written without a time:
2 Jan 2006
In this case, the time will be interpreted as 10am UTC on that date.
The tags line is a comma-separated list of tags that may be used to categorize
the document.
The author section may contain a mixture of text, twitter names, and links.
For slide presentations, only the plain text lines will be displayed on the
first slide.
Multiple presenters may be specified, separated by a blank line.
After that come slides/sections, each after a blank line:
* Title of slide or section (must have asterisk)
Some Text
** Subsection
- bullets
- more bullets
- a bullet with
*** Sub-subsection
Some More text
Preformatted text
is indented (however you like)
Further Text, including invocations like:
.code x.go /^func main/,/^}/
.play y.go
.image image.jpg
.iframe http://foo
.link http://foo label
.html file.html
Again, more text
Blank lines are OK (not mandatory) after the title and after the
text. Text, bullets, and .code etc. are all optional; title is
not.
Lines starting with # in column 1 are commentary.
Fonts:
Within the input for plain text or lists, text bracketed by font
markers will be presented in italic, bold, or program font.
Marker characters are _ (italic), * (bold) and ` (program font).
Unmatched markers appear as plain text.
Within marked text, a single marker character becomes a space
and a doubled single marker quotes the marker character.
_italic_
*bold*
`program`
_this_is_all_italic_
_Why_use_scoped__ptr_? Use plain ***ptr* instead.
Inline links:
Links can be included in any text with the form [[url][label]], or
[[url]] to use the URL itself as the label.
Functions:
A number of template functions are available through invocations
in the input text. Each such invocation contains a period as the
first character on the line, followed immediately by the name of
the function, followed by any arguments. A typical invocation might
be
.play demo.go /^func show/,/^}/
(except that the ".play" must be at the beginning of the line and
not be indented like this.)
Here follows a description of the functions:
code:
Injects program source into the output by extracting code from files
and injecting them as HTML-escaped <pre> 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

31
present/html.go Normal file
View File

@ -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" }

45
present/iframe.go Normal file
View File

@ -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
}

45
present/image.go Normal file
View File

@ -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
}

88
present/link.go Normal file
View File

@ -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(`<a href="%s" target="_blank">%s</a>`, 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:
// <a href="http://golang.org">golang.org</a>
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
}

40
present/link_test.go Normal file
View File

@ -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", "<b>link</b>", 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)
}
}
}

495
present/parse.go Normal file
View File

@ -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
}

166
present/style.go Normal file
View File

@ -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
<i>this is italic</i>!
*/
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 += "<i>"
close = "</i>"
case '*':
open += "<b>"
close = "</b>"
case '`':
open += "<code>"
close = "</code>"
}
// 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
}

116
present/style_test.go Normal file
View File

@ -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_", "<i>a</i>"},
{"*a*", "<b>a</b>"},
{"`a`", "<code>a</code>"},
{"_a_b_", "<i>a b</i>"},
{"_a__b_", "<i>a_b</i>"},
{"_a___b_", "<i>a_ b</i>"},
{"*a**b*?", "<b>a*b</b>?"},
{"_a_<>_b_.", "<i>a <> b</i>."},
{"(_a_)", "(<i>a</i>)"},
{"((_a_), _b_, _c_).", "((<i>a</i>), <i>b</i>, <i>c</i>)."},
{"(_a)", "(_a)"},
{"(_a)", "(_a)"},
{"_Why_use_scoped__ptr_? Use plain ***ptr* instead.", "<i>Why use scoped_ptr</i>? Use plain <b>*ptr</b> instead."},
{"_hey_ [[http://golang.org][*Gophers*]] *around*",
`<i>hey</i> <a href="http://golang.org" target="_blank"><b>Gophers</b></a> <b>around</b>`},
{"_hey_ [[http://golang.org][so _many_ *Gophers*]] *around*",
`<i>hey</i> <a href="http://golang.org" target="_blank">so <i>many</i> <b>Gophers</b></a> <b>around</b>`},
{"Visit [[http://golang.org]] now",
`Visit <a href="http://golang.org" target="_blank">golang.org</a> now`},
{"my talk ([[http://talks.golang.org/][slides here]])",
`my talk (<a href="http://talks.golang.org/" target="_blank">slides here</a>)`},
}
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_", "<i>a</i>"},
{"*a*", "<b>a</b>"},
{"`a`", "<code>a</code>"},
{"_a_b_", "<i>a b</i>"},
{"_a__b_", "<i>a_b</i>"},
{"_a___b_", "<i>a_ b</i>"},
{"*a**b*?", "<b>a*b</b>?"},
{"_a_<>_b_.", "<i>a &lt;&gt; b</i>."},
{"(_a_<>_b_)", "(<i>a &lt;&gt; b</i>)"},
{"((_a_), _b_, _c_).", "((<i>a</i>), <i>b</i>, <i>c</i>)."},
{"(_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: <b>Gophers</b> are <i>clearly</i> &gt; <b>cats</b>!
}