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