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