github.com/jgarto/itcv@v0.0.0-20180826224514-4eea09c1aa0d/_vendor/src/golang.org/x/tools/present/parse.go (about)

     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  
     5  package present
     6  
     7  import (
     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  
    24  var (
    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.
    30  func Template() *template.Template {
    31  	return template.New("").Funcs(funcs)
    32  }
    33  
    34  // Render renders the doc to the given writer using the provided template.
    35  func (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.
    46  func (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  
    55  type 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.
    60  func 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.
    68  type 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.
    79  type 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.
    86  func (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.
    98  type 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
   108  func (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.
   125  func (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.
   136  func (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.
   142  func (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  
   150  func (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.
   154  type Elem interface {
   155  	TemplateName() string
   156  }
   157  
   158  // renderElem implements the elem template function, used to render
   159  // sub-templates.
   160  func 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.
   172  func pageNum(s Section, offset int) int {
   173  	if len(s.Number) == 0 {
   174  		return offset
   175  	}
   176  	return s.Number[0] + offset
   177  }
   178  
   179  func 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.
   186  func 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.
   196  type Text struct {
   197  	Lines []string
   198  	Pre   bool
   199  }
   200  
   201  func (t Text) TemplateName() string { return "text" }
   202  
   203  // List represents a bulleted list.
   204  type List struct {
   205  	Bullet []string
   206  }
   207  
   208  func (l List) TemplateName() string { return "list" }
   209  
   210  // Lines is a helper for parsing line-based input.
   211  type Lines struct {
   212  	line int // 0 indexed, so has 1-indexed number of last line returned
   213  	text []string
   214  }
   215  
   216  func 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  
   228  func (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  
   245  func (l *Lines) back() {
   246  	l.line--
   247  }
   248  
   249  func (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.
   263  type 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.
   269  type ParseMode int
   270  
   271  const (
   272  	// If set, parse only the title and subtitle.
   273  	TitlesOnly ParseMode = 1
   274  )
   275  
   276  // Parse parses a document from r.
   277  func (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.
   315  func 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.
   321  var 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.
   325  func 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).
   331  func 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\n", 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  
   442  func 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  
   478  func 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  
   543  func 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  
   552  func 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  
   566  func isSpeakerNote(s string) bool {
   567  	return strings.HasPrefix(s, ": ")
   568  }