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 }