You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

559 lines
13 KiB

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