|
|
// 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 NotesEnabled bool }{d, t, PlayEnabled, NotesEnabled} 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 TitleNotes []string 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 Notes []string Classes []string Styles []string }
// HTMLAttributes for the section
func (s Section) HTMLAttributes() template.HTMLAttr { if len(s.Classes) == 0 && len(s.Styles) == 0 { return "" }
var class string if len(s.Classes) > 0 { class = fmt.Sprintf(`class=%q`, strings.Join(s.Classes, " ")) } var style string if len(s.Styles) > 0 { style = fmt.Sprintf(`style=%q`, strings.Join(s.Styles, " ")) } return template.HTMLAttr(strings.Join([]string{class, style}, " ")) }
// Sections contained within the section.
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 }
for i := lines.line; i < len(lines.text); i++ { if strings.HasPrefix(lines.text[i], "*") { break }
if isSpeakerNote(lines.text[i]) { doc.TitleNotes = append(doc.TitleNotes, lines.text[i][2:]) } }
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{}); 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) ([]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 isSpeakerNote(text): section.Notes = append(section.Notes, text[2:]) case strings.HasPrefix(text, prefix+"* "): lines.back() subsecs, err := parseSections(ctx, name, lines, section.Number) if err != nil { return nil, err } for _, ss := range subsecs { section.Elem = append(section.Elem, ss) } case strings.HasPrefix(text, "."): args := strings.Fields(text) if args[0] == ".background" { section.Classes = append(section.Classes, "background") section.Styles = append(section.Styles, "background-image: url('"+args[1]+"')") break } 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.
lines.back() 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 } if isSpeakerNote(text) { continue } 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 isSpeakerNote(text) { continue }
// 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 }
func isSpeakerNote(s string) bool { return strings.HasPrefix(s, ": ") }
|