506 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			506 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
// 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)
 | 
						|
}
 | 
						|
 | 
						|
// Render renders the section to the given writer using the provided template.
 | 
						|
func (s *Section) Render(w io.Writer, t *template.Template) error {
 | 
						|
	data := struct {
 | 
						|
		*Section
 | 
						|
		Template    *template.Template
 | 
						|
		PlayEnabled bool
 | 
						|
	}{s, t, PlayEnabled}
 | 
						|
	return t.ExecuteTemplate(w, "section", 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
 | 
						|
}
 |