1// Copyright 2011 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package present
6
7import (
8	"bufio"
9	"bytes"
10	"errors"
11	"fmt"
12	"html/template"
13	"io"
14	"io/ioutil"
15	"log"
16	"net/url"
17	"regexp"
18	"strings"
19	"time"
20	"unicode"
21	"unicode/utf8"
22)
23
24var (
25	parsers = make(map[string]ParseFunc)
26	funcs   = template.FuncMap{}
27)
28
29// Template returns an empty template with the action functions in its FuncMap.
30func Template() *template.Template {
31	return template.New("").Funcs(funcs)
32}
33
34// Render renders the doc to the given writer using the provided template.
35func (d *Doc) Render(w io.Writer, t *template.Template) error {
36	data := struct {
37		*Doc
38		Template     *template.Template
39		PlayEnabled  bool
40		NotesEnabled bool
41	}{d, t, PlayEnabled, NotesEnabled}
42	return t.ExecuteTemplate(w, "root", data)
43}
44
45// Render renders the section to the given writer using the provided template.
46func (s *Section) Render(w io.Writer, t *template.Template) error {
47	data := struct {
48		*Section
49		Template    *template.Template
50		PlayEnabled bool
51	}{s, t, PlayEnabled}
52	return t.ExecuteTemplate(w, "section", data)
53}
54
55type ParseFunc func(ctx *Context, fileName string, lineNumber int, inputLine string) (Elem, error)
56
57// Register binds the named action, which does not begin with a period, to the
58// specified parser to be invoked when the name, with a period, appears in the
59// present input text.
60func Register(name string, parser ParseFunc) {
61	if len(name) == 0 || name[0] == ';' {
62		panic("bad name in Register: " + name)
63	}
64	parsers["."+name] = parser
65}
66
67// Doc represents an entire document.
68type Doc struct {
69	Title      string
70	Subtitle   string
71	Time       time.Time
72	Authors    []Author
73	TitleNotes []string
74	Sections   []Section
75	Tags       []string
76}
77
78// Author represents the person who wrote and/or is presenting the document.
79type Author struct {
80	Elem []Elem
81}
82
83// TextElem returns the first text elements of the author details.
84// This is used to display the author' name, job title, and company
85// without the contact details.
86func (p *Author) TextElem() (elems []Elem) {
87	for _, el := range p.Elem {
88		if _, ok := el.(Text); !ok {
89			break
90		}
91		elems = append(elems, el)
92	}
93	return
94}
95
96// Section represents a section of a document (such as a presentation slide)
97// comprising a title and a list of elements.
98type Section struct {
99	Number  []int
100	Title   string
101	Elem    []Elem
102	Notes   []string
103	Classes []string
104	Styles  []string
105}
106
107// HTMLAttributes for the section
108func (s Section) HTMLAttributes() template.HTMLAttr {
109	if len(s.Classes) == 0 && len(s.Styles) == 0 {
110		return ""
111	}
112
113	var class string
114	if len(s.Classes) > 0 {
115		class = fmt.Sprintf(`class=%q`, strings.Join(s.Classes, " "))
116	}
117	var style string
118	if len(s.Styles) > 0 {
119		style = fmt.Sprintf(`style=%q`, strings.Join(s.Styles, " "))
120	}
121	return template.HTMLAttr(strings.Join([]string{class, style}, " "))
122}
123
124// Sections contained within the section.
125func (s Section) Sections() (sections []Section) {
126	for _, e := range s.Elem {
127		if section, ok := e.(Section); ok {
128			sections = append(sections, section)
129		}
130	}
131	return
132}
133
134// Level returns the level of the given section.
135// The document title is level 1, main section 2, etc.
136func (s Section) Level() int {
137	return len(s.Number) + 1
138}
139
140// FormattedNumber returns a string containing the concatenation of the
141// numbers identifying a Section.
142func (s Section) FormattedNumber() string {
143	b := &bytes.Buffer{}
144	for _, n := range s.Number {
145		fmt.Fprintf(b, "%v.", n)
146	}
147	return b.String()
148}
149
150func (s Section) TemplateName() string { return "section" }
151
152// Elem defines the interface for a present element. That is, something that
153// can provide the name of the template used to render the element.
154type Elem interface {
155	TemplateName() string
156}
157
158// renderElem implements the elem template function, used to render
159// sub-templates.
160func renderElem(t *template.Template, e Elem) (template.HTML, error) {
161	var data interface{} = e
162	if s, ok := e.(Section); ok {
163		data = struct {
164			Section
165			Template *template.Template
166		}{s, t}
167	}
168	return execTemplate(t, e.TemplateName(), data)
169}
170
171// pageNum derives a page number from a section.
172func pageNum(s Section, offset int) int {
173	if len(s.Number) == 0 {
174		return offset
175	}
176	return s.Number[0] + offset
177}
178
179func init() {
180	funcs["elem"] = renderElem
181	funcs["pagenum"] = pageNum
182}
183
184// execTemplate is a helper to execute a template and return the output as a
185// template.HTML value.
186func execTemplate(t *template.Template, name string, data interface{}) (template.HTML, error) {
187	b := new(bytes.Buffer)
188	err := t.ExecuteTemplate(b, name, data)
189	if err != nil {
190		return "", err
191	}
192	return template.HTML(b.String()), nil
193}
194
195// Text represents an optionally preformatted paragraph.
196type Text struct {
197	Lines []string
198	Pre   bool
199}
200
201func (t Text) TemplateName() string { return "text" }
202
203// List represents a bulleted list.
204type List struct {
205	Bullet []string
206}
207
208func (l List) TemplateName() string { return "list" }
209
210// Lines is a helper for parsing line-based input.
211type Lines struct {
212	line int // 0 indexed, so has 1-indexed number of last line returned
213	text []string
214}
215
216func readLines(r io.Reader) (*Lines, error) {
217	var lines []string
218	s := bufio.NewScanner(r)
219	for s.Scan() {
220		lines = append(lines, s.Text())
221	}
222	if err := s.Err(); err != nil {
223		return nil, err
224	}
225	return &Lines{0, lines}, nil
226}
227
228func (l *Lines) next() (text string, ok bool) {
229	for {
230		current := l.line
231		l.line++
232		if current >= len(l.text) {
233			return "", false
234		}
235		text = l.text[current]
236		// Lines starting with # are comments.
237		if len(text) == 0 || text[0] != '#' {
238			ok = true
239			break
240		}
241	}
242	return
243}
244
245func (l *Lines) back() {
246	l.line--
247}
248
249func (l *Lines) nextNonEmpty() (text string, ok bool) {
250	for {
251		text, ok = l.next()
252		if !ok {
253			return
254		}
255		if len(text) > 0 {
256			break
257		}
258	}
259	return
260}
261
262// A Context specifies the supporting context for parsing a presentation.
263type Context struct {
264	// ReadFile reads the file named by filename and returns the contents.
265	ReadFile func(filename string) ([]byte, error)
266}
267
268// ParseMode represents flags for the Parse function.
269type ParseMode int
270
271const (
272	// If set, parse only the title and subtitle.
273	TitlesOnly ParseMode = 1
274)
275
276// Parse parses a document from r.
277func (ctx *Context) Parse(r io.Reader, name string, mode ParseMode) (*Doc, error) {
278	doc := new(Doc)
279	lines, err := readLines(r)
280	if err != nil {
281		return nil, err
282	}
283
284	for i := lines.line; i < len(lines.text); i++ {
285		if strings.HasPrefix(lines.text[i], "*") {
286			break
287		}
288
289		if isSpeakerNote(lines.text[i]) {
290			doc.TitleNotes = append(doc.TitleNotes, lines.text[i][2:])
291		}
292	}
293
294	err = parseHeader(doc, lines)
295	if err != nil {
296		return nil, err
297	}
298	if mode&TitlesOnly != 0 {
299		return doc, nil
300	}
301
302	// Authors
303	if doc.Authors, err = parseAuthors(lines); err != nil {
304		return nil, err
305	}
306	// Sections
307	if doc.Sections, err = parseSections(ctx, name, lines, []int{}); err != nil {
308		return nil, err
309	}
310	return doc, nil
311}
312
313// Parse parses a document from r. Parse reads assets used by the presentation
314// from the file system using ioutil.ReadFile.
315func Parse(r io.Reader, name string, mode ParseMode) (*Doc, error) {
316	ctx := Context{ReadFile: ioutil.ReadFile}
317	return ctx.Parse(r, name, mode)
318}
319
320// isHeading matches any section heading.
321var isHeading = regexp.MustCompile(`^\*+ `)
322
323// lesserHeading returns true if text is a heading of a lesser or equal level
324// than that denoted by prefix.
325func lesserHeading(text, prefix string) bool {
326	return isHeading.MatchString(text) && !strings.HasPrefix(text, prefix+"*")
327}
328
329// parseSections parses Sections from lines for the section level indicated by
330// number (a nil number indicates the top level).
331func parseSections(ctx *Context, name string, lines *Lines, number []int) ([]Section, error) {
332	var sections []Section
333	for i := 1; ; i++ {
334		// Next non-empty line is title.
335		text, ok := lines.nextNonEmpty()
336		for ok && text == "" {
337			text, ok = lines.next()
338		}
339		if !ok {
340			break
341		}
342		prefix := strings.Repeat("*", len(number)+1)
343		if !strings.HasPrefix(text, prefix+" ") {
344			lines.back()
345			break
346		}
347		section := Section{
348			Number: append(append([]int{}, number...), i),
349			Title:  text[len(prefix)+1:],
350		}
351		text, ok = lines.nextNonEmpty()
352		for ok && !lesserHeading(text, prefix) {
353			var e Elem
354			r, _ := utf8.DecodeRuneInString(text)
355			switch {
356			case unicode.IsSpace(r):
357				i := strings.IndexFunc(text, func(r rune) bool {
358					return !unicode.IsSpace(r)
359				})
360				if i < 0 {
361					break
362				}
363				indent := text[:i]
364				var s []string
365				for ok && (strings.HasPrefix(text, indent) || text == "") {
366					if text != "" {
367						text = text[i:]
368					}
369					s = append(s, text)
370					text, ok = lines.next()
371				}
372				lines.back()
373				pre := strings.Join(s, "\n")
374				pre = strings.Replace(pre, "\t", "    ", -1) // browsers treat tabs badly
375				pre = strings.TrimRightFunc(pre, unicode.IsSpace)
376				e = Text{Lines: []string{pre}, Pre: true}
377			case strings.HasPrefix(text, "- "):
378				var b []string
379				for ok && strings.HasPrefix(text, "- ") {
380					b = append(b, text[2:])
381					text, ok = lines.next()
382				}
383				lines.back()
384				e = List{Bullet: b}
385			case isSpeakerNote(text):
386				section.Notes = append(section.Notes, text[2:])
387			case strings.HasPrefix(text, prefix+"* "):
388				lines.back()
389				subsecs, err := parseSections(ctx, name, lines, section.Number)
390				if err != nil {
391					return nil, err
392				}
393				for _, ss := range subsecs {
394					section.Elem = append(section.Elem, ss)
395				}
396			case strings.HasPrefix(text, "."):
397				args := strings.Fields(text)
398				if args[0] == ".background" {
399					section.Classes = append(section.Classes, "background")
400					section.Styles = append(section.Styles, "background-image: url('"+args[1]+"')")
401					break
402				}
403				parser := parsers[args[0]]
404				if parser == nil {
405					return nil, fmt.Errorf("%s:%d: unknown command %q", name, lines.line, text)
406				}
407				t, err := parser(ctx, name, lines.line, text)
408				if err != nil {
409					return nil, err
410				}
411				e = t
412			default:
413				var l []string
414				for ok && strings.TrimSpace(text) != "" {
415					if text[0] == '.' { // Command breaks text block.
416						lines.back()
417						break
418					}
419					if strings.HasPrefix(text, `\.`) { // Backslash escapes initial period.
420						text = text[1:]
421					}
422					l = append(l, text)
423					text, ok = lines.next()
424				}
425				if len(l) > 0 {
426					e = Text{Lines: l}
427				}
428			}
429			if e != nil {
430				section.Elem = append(section.Elem, e)
431			}
432			text, ok = lines.nextNonEmpty()
433		}
434		if isHeading.MatchString(text) {
435			lines.back()
436		}
437		sections = append(sections, section)
438	}
439	return sections, nil
440}
441
442func parseHeader(doc *Doc, lines *Lines) error {
443	var ok bool
444	// First non-empty line starts header.
445	doc.Title, ok = lines.nextNonEmpty()
446	if !ok {
447		return errors.New("unexpected EOF; expected title")
448	}
449	for {
450		text, ok := lines.next()
451		if !ok {
452			return errors.New("unexpected EOF")
453		}
454		if text == "" {
455			break
456		}
457		if isSpeakerNote(text) {
458			continue
459		}
460		const tagPrefix = "Tags:"
461		if strings.HasPrefix(text, tagPrefix) {
462			tags := strings.Split(text[len(tagPrefix):], ",")
463			for i := range tags {
464				tags[i] = strings.TrimSpace(tags[i])
465			}
466			doc.Tags = append(doc.Tags, tags...)
467		} else if t, ok := parseTime(text); ok {
468			doc.Time = t
469		} else if doc.Subtitle == "" {
470			doc.Subtitle = text
471		} else {
472			return fmt.Errorf("unexpected header line: %q", text)
473		}
474	}
475	return nil
476}
477
478func parseAuthors(lines *Lines) (authors []Author, err error) {
479	// This grammar demarcates authors with blanks.
480
481	// Skip blank lines.
482	if _, ok := lines.nextNonEmpty(); !ok {
483		return nil, errors.New("unexpected EOF")
484	}
485	lines.back()
486
487	var a *Author
488	for {
489		text, ok := lines.next()
490		if !ok {
491			return nil, errors.New("unexpected EOF")
492		}
493
494		// If we find a section heading, we're done.
495		if strings.HasPrefix(text, "* ") {
496			lines.back()
497			break
498		}
499
500		if isSpeakerNote(text) {
501			continue
502		}
503
504		// If we encounter a blank we're done with this author.
505		if a != nil && len(text) == 0 {
506			authors = append(authors, *a)
507			a = nil
508			continue
509		}
510		if a == nil {
511			a = new(Author)
512		}
513
514		// Parse the line. Those that
515		// - begin with @ are twitter names,
516		// - contain slashes are links, or
517		// - contain an @ symbol are an email address.
518		// The rest is just text.
519		var el Elem
520		switch {
521		case strings.HasPrefix(text, "@"):
522			el = parseURL("http://twitter.com/" + text[1:])
523		case strings.Contains(text, ":"):
524			el = parseURL(text)
525		case strings.Contains(text, "@"):
526			el = parseURL("mailto:" + text)
527		}
528		if l, ok := el.(Link); ok {
529			l.Label = text
530			el = l
531		}
532		if el == nil {
533			el = Text{Lines: []string{text}}
534		}
535		a.Elem = append(a.Elem, el)
536	}
537	if a != nil {
538		authors = append(authors, *a)
539	}
540	return authors, nil
541}
542
543func parseURL(text string) Elem {
544	u, err := url.Parse(text)
545	if err != nil {
546		log.Printf("Parse(%q): %v", text, err)
547		return nil
548	}
549	return Link{URL: u}
550}
551
552func parseTime(text string) (t time.Time, ok bool) {
553	t, err := time.Parse("15:04 2 Jan 2006", text)
554	if err == nil {
555		return t, true
556	}
557	t, err = time.Parse("2 Jan 2006", text)
558	if err == nil {
559		// at 11am UTC it is the same date everywhere
560		t = t.Add(time.Hour * 11)
561		return t, true
562	}
563	return time.Time{}, false
564}
565
566func isSpeakerNote(s string) bool {
567	return strings.HasPrefix(s, ": ")
568}
569