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