зеркало из https://github.com/golang/tools.git
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:
Родитель
a76da35c40
Коммит
9fc516408c
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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" }
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 <> b</i>."},
|
||||
{"(_a_<>_b_)", "(<i>a <> 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> > <b>cats</b>!
|
||||
}
|
Загрузка…
Ссылка в новой задаче