mirror of
https://github.com/golang/go.git
synced 2025-05-05 23:53:05 +00: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:
parent
a76da35c40
commit
9fc516408c
229
present/args.go
Normal file
229
present/args.go
Normal 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
252
present/code.go
Normal 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
252
present/code.go.orig
Normal 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
184
present/doc.go
Normal 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
31
present/html.go
Normal 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
45
present/iframe.go
Normal 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
45
present/image.go
Normal 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
88
present/link.go
Normal 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
40
present/link_test.go
Normal 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
495
present/parse.go
Normal 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
166
present/style.go
Normal 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
116
present/style_test.go
Normal 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 <> 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>!
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user