whatcanGOwrong

This commit is contained in:
2024-09-19 21:38:24 -04:00
commit d0ae4d841d
17908 changed files with 4096831 additions and 0 deletions
@@ -0,0 +1,228 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package present
import (
"errors"
"regexp"
"strconv"
"unicode/utf8"
)
// This file is stolen from go/src/cmd/godoc/codewalk.go.
// It's an evaluator for the file address syntax implemented by acme and sam,
// but using Go-native regular expressions.
// To keep things reasonably close, this version uses (?m:re) for all user-provided
// regular expressions. That is the only change to the code from codewalk.go.
// See http://9p.io/sys/doc/sam/sam.html Table II for details on the syntax.
// addrToByteRange evaluates the given address starting at offset start in data.
// It returns the lo and hi byte offset of the matched region within data.
func addrToByteRange(addr string, start int, data []byte) (lo, hi int, err error) {
if addr == "" {
lo, hi = start, len(data)
return
}
var (
dir byte
prevc byte
charOffset bool
)
lo = start
hi = start
for addr != "" && err == nil {
c := addr[0]
switch c {
default:
err = errors.New("invalid address syntax near " + string(c))
case ',':
if len(addr) == 1 {
hi = len(data)
} else {
_, hi, err = addrToByteRange(addr[1:], hi, data)
}
return
case '+', '-':
if prevc == '+' || prevc == '-' {
lo, hi, err = addrNumber(data, lo, hi, prevc, 1, charOffset)
}
dir = c
case '$':
lo = len(data)
hi = len(data)
if len(addr) > 1 {
dir = '+'
}
case '#':
charOffset = true
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
var i int
for i = 1; i < len(addr); i++ {
if addr[i] < '0' || addr[i] > '9' {
break
}
}
var n int
n, err = strconv.Atoi(addr[0:i])
if err != nil {
break
}
lo, hi, err = addrNumber(data, lo, hi, dir, n, charOffset)
dir = 0
charOffset = false
prevc = c
addr = addr[i:]
continue
case '/':
var i, j int
Regexp:
for i = 1; i < len(addr); i++ {
switch addr[i] {
case '\\':
i++
case '/':
j = i + 1
break Regexp
}
}
if j == 0 {
j = i
}
pattern := addr[1:i]
lo, hi, err = addrRegexp(data, lo, hi, dir, pattern)
prevc = c
addr = addr[j:]
continue
}
prevc = c
addr = addr[1:]
}
if err == nil && dir != 0 {
lo, hi, err = addrNumber(data, lo, hi, dir, 1, charOffset)
}
if err != nil {
return 0, 0, err
}
return lo, hi, nil
}
// addrNumber applies the given dir, n, and charOffset to the address lo, hi.
// dir is '+' or '-', n is the count, and charOffset is true if the syntax
// used was #n. Applying +n (or +#n) means to advance n lines
// (or characters) after hi. Applying -n (or -#n) means to back up n lines
// (or characters) before lo.
// The return value is the new lo, hi.
func addrNumber(data []byte, lo, hi int, dir byte, n int, charOffset bool) (int, int, error) {
switch dir {
case 0:
lo = 0
hi = 0
fallthrough
case '+':
if charOffset {
pos := hi
for ; n > 0 && pos < len(data); n-- {
_, size := utf8.DecodeRune(data[pos:])
pos += size
}
if n == 0 {
return pos, pos, nil
}
break
}
// find next beginning of line
if hi > 0 {
for hi < len(data) && data[hi-1] != '\n' {
hi++
}
}
lo = hi
if n == 0 {
return lo, hi, nil
}
for ; hi < len(data); hi++ {
if data[hi] != '\n' {
continue
}
switch n--; n {
case 1:
lo = hi + 1
case 0:
return lo, hi + 1, nil
}
}
case '-':
if charOffset {
// Scan backward for bytes that are not UTF-8 continuation bytes.
pos := lo
for ; pos > 0 && n > 0; pos-- {
if data[pos]&0xc0 != 0x80 {
n--
}
}
if n == 0 {
return pos, pos, nil
}
break
}
// find earlier beginning of line
for lo > 0 && data[lo-1] != '\n' {
lo--
}
hi = lo
if n == 0 {
return lo, hi, nil
}
for ; lo >= 0; lo-- {
if lo > 0 && data[lo-1] != '\n' {
continue
}
switch n--; n {
case 1:
hi = lo
case 0:
return lo, hi, nil
}
}
}
return 0, 0, errors.New("address out of range")
}
// addrRegexp searches for pattern in the given direction starting at lo, hi.
// The direction dir is '+' (search forward from hi) or '-' (search backward from lo).
// Backward searches are unimplemented.
func addrRegexp(data []byte, lo, hi int, dir byte, pattern string) (int, int, error) {
// We want ^ and $ to work as in sam/acme, so use ?m.
re, err := regexp.Compile("(?m:" + pattern + ")")
if err != nil {
return 0, 0, err
}
if dir == '-' {
// Could implement reverse search using binary search
// through file, but that seems like overkill.
return 0, 0, errors.New("reverse search not implemented")
}
m := re.FindIndex(data[hi:])
if len(m) > 0 {
m[0] += hi
m[1] += hi
} else if hi > 0 {
// No match. Wrap to beginning of data.
m = re.FindIndex(data)
}
if len(m) == 0 {
return 0, 0, errors.New("no match for " + pattern)
}
return m[0], m[1], nil
}
@@ -0,0 +1,24 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package present
import "strings"
func init() {
Register("caption", parseCaption)
}
type Caption struct {
Cmd string // original command from present source
Text string
}
func (c Caption) PresentCmd() string { return c.Cmd }
func (c Caption) TemplateName() string { return "caption" }
func parseCaption(_ *Context, _ string, _ int, cmd string) (Elem, error) {
text := strings.TrimSpace(strings.TrimPrefix(cmd, ".caption"))
return Caption{cmd, text}, nil
}
@@ -0,0 +1,272 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package present
import (
"bufio"
"bytes"
"fmt"
"html/template"
"path/filepath"
"regexp"
"strconv"
"strings"
)
// PlayEnabled specifies whether runnable playground snippets should be
// displayed in the present user interface.
var PlayEnabled = false
// TODO(adg): replace the PlayEnabled flag with something less spaghetti-like.
// Instead this will probably be determined by a template execution Context
// value that contains various global metadata required when rendering
// templates.
// NotesEnabled specifies whether presenter notes should be displayed in the
// present user interface.
var NotesEnabled = false
func init() {
Register("code", parseCode)
Register("play", parseCode)
}
type Code struct {
Cmd string // original command from present source
Text template.HTML
Play bool // runnable code
Edit bool // editable code
FileName string // file name
Ext string // file extension
Raw []byte // content of the file
}
func (c Code) PresentCmd() string { return c.Cmd }
func (c Code) TemplateName() string { return "code" }
// The input line is a .code or .play entry with a file name and an optional HLfoo marker on the end.
// Anything between the file and HL (if any) is an address expression, which we treat as a string here.
// We pick off the HL first, for easy parsing.
var (
highlightRE = regexp.MustCompile(`\s+HL([a-zA-Z0-9_]+)?$`)
hlCommentRE = regexp.MustCompile(`(.+) // HL(.*)$`)
codeRE = regexp.MustCompile(`\.(code|play)\s+((?:(?:-edit|-numbers)\s+)*)([^\s]+)(?:\s+(.*))?$`)
)
// parseCode parses a code present directive. Its syntax:
//
// .code [-numbers] [-edit] <filename> [address] [highlight]
//
// The directive may also be ".play" if the snippet is executable.
func parseCode(ctx *Context, sourceFile string, sourceLine int, cmd string) (Elem, error) {
cmd = strings.TrimSpace(cmd)
origCmd := cmd
// Pull off the HL, if any, from the end of the input line.
highlight := ""
if hl := highlightRE.FindStringSubmatchIndex(cmd); len(hl) == 4 {
if hl[2] < 0 || hl[3] < 0 {
return nil, fmt.Errorf("%s:%d invalid highlight syntax", sourceFile, sourceLine)
}
highlight = cmd[hl[2]:hl[3]]
cmd = cmd[:hl[2]-2]
}
// Parse the remaining command line.
// Arguments:
// args[0]: whole match
// args[1]: .code/.play
// args[2]: flags ("-edit -numbers")
// args[3]: file name
// args[4]: optional address
args := codeRE.FindStringSubmatch(cmd)
if len(args) != 5 {
return nil, fmt.Errorf("%s:%d: syntax error for .code/.play invocation", sourceFile, sourceLine)
}
command, flags, file, addr := args[1], args[2], args[3], strings.TrimSpace(args[4])
play := command == "play" && PlayEnabled
// Read in code file and (optionally) match address.
filename := filepath.Join(filepath.Dir(sourceFile), file)
textBytes, err := ctx.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err)
}
lo, hi, err := addrToByteRange(addr, 0, textBytes)
if err != nil {
return nil, fmt.Errorf("%s:%d: %v", sourceFile, sourceLine, err)
}
if lo > hi {
// The search in addrToByteRange can wrap around so we might
// end up with the range ending before its starting point
hi, lo = lo, hi
}
// Acme pattern matches can stop mid-line,
// so run to end of line in both directions if not at line start/end.
for lo > 0 && textBytes[lo-1] != '\n' {
lo--
}
if hi > 0 {
for hi < len(textBytes) && textBytes[hi-1] != '\n' {
hi++
}
}
lines := codeLines(textBytes, lo, hi)
data := &codeTemplateData{
Lines: formatLines(lines, highlight),
Edit: strings.Contains(flags, "-edit"),
Numbers: strings.Contains(flags, "-numbers"),
}
// Include before and after in a hidden span for playground code.
if play {
data.Prefix = textBytes[:lo]
data.Suffix = textBytes[hi:]
}
var buf bytes.Buffer
if err := codeTemplate.Execute(&buf, data); err != nil {
return nil, err
}
return Code{
Cmd: origCmd,
Text: template.HTML(buf.String()),
Play: play,
Edit: data.Edit,
FileName: filepath.Base(filename),
Ext: filepath.Ext(filename),
Raw: rawCode(lines),
}, nil
}
// formatLines returns a new slice of codeLine with the given lines
// replacing tabs with spaces and adding highlighting where needed.
func formatLines(lines []codeLine, highlight string) []codeLine {
formatted := make([]codeLine, len(lines))
for i, line := range lines {
// Replace tabs with spaces, which work better in HTML.
line.L = strings.Replace(line.L, "\t", " ", -1)
// Highlight lines that end with "// HL[highlight]"
// and strip the magic comment.
if m := hlCommentRE.FindStringSubmatch(line.L); m != nil {
line.L = m[1]
line.HL = m[2] == highlight
}
formatted[i] = line
}
return formatted
}
// rawCode returns the code represented by the given codeLines without any kind
// of formatting.
func rawCode(lines []codeLine) []byte {
b := new(bytes.Buffer)
for _, line := range lines {
b.WriteString(line.L)
b.WriteByte('\n')
}
return b.Bytes()
}
type codeTemplateData struct {
Lines []codeLine
Prefix, Suffix []byte
Edit, Numbers bool
}
var leadingSpaceRE = regexp.MustCompile(`^[ \t]*`)
var codeTemplate = template.Must(template.New("code").Funcs(template.FuncMap{
"trimSpace": strings.TrimSpace,
"leadingSpace": leadingSpaceRE.FindString,
}).Parse(codeTemplateHTML))
const codeTemplateHTML = `
{{with .Prefix}}<pre style="display: none"><span>{{printf "%s" .}}</span></pre>{{end -}}
<pre{{if .Edit}} contenteditable="true" spellcheck="false"{{end}}{{if .Numbers}} class="numbers"{{end}}>{{/*
*/}}{{range .Lines}}<span num="{{.N}}">{{/*
*/}}{{if .HL}}{{leadingSpace .L}}<b>{{trimSpace .L}}</b>{{/*
*/}}{{else}}{{.L}}{{end}}{{/*
*/}}</span>
{{end}}</pre>
{{with .Suffix}}<pre style="display: none"><span>{{printf "%s" .}}</span></pre>{{end -}}
`
// codeLine represents a line of code extracted from a source file.
type codeLine struct {
L string // The line of code.
N int // The line number from the source file.
HL bool // Whether the line should be highlighted.
}
// codeLines takes a source file and returns the lines that
// span the byte range specified by start and end.
// It discards lines that end in "OMIT".
func codeLines(src []byte, start, end int) (lines []codeLine) {
startLine := 1
for i, b := range src {
if i == start {
break
}
if b == '\n' {
startLine++
}
}
s := bufio.NewScanner(bytes.NewReader(src[start:end]))
for n := startLine; s.Scan(); n++ {
l := s.Text()
if strings.HasSuffix(l, "OMIT") {
continue
}
lines = append(lines, codeLine{L: l, N: n})
}
// Trim leading and trailing blank lines.
for len(lines) > 0 && len(lines[0].L) == 0 {
lines = lines[1:]
}
for len(lines) > 0 && len(lines[len(lines)-1].L) == 0 {
lines = lines[:len(lines)-1]
}
return
}
func parseArgs(name string, line int, args []string) (res []interface{}, err error) {
res = make([]interface{}, len(args))
for i, v := range args {
if len(v) == 0 {
return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v)
}
switch v[0] {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
n, err := strconv.Atoi(v)
if err != nil {
return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v)
}
res[i] = n
case '/':
if len(v) < 2 || v[len(v)-1] != '/' {
return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v)
}
res[i] = v
case '$':
res[i] = "$"
case '_':
if len(v) == 1 {
// Do nothing; "_" indicates an intentionally empty parameter.
break
}
fallthrough
default:
return nil, fmt.Errorf("%s:%d bad code argument %q", name, line, v)
}
}
return
}
@@ -0,0 +1,225 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package present
import (
"fmt"
"html/template"
"strings"
"testing"
)
func TestParseCode(t *testing.T) {
// Enable play but revert the change at the end.
defer func(play bool) { PlayEnabled = play }(PlayEnabled)
PlayEnabled = true
helloTest := []byte(`
package main
import "fmt"
func main() {
fmt.Println("hello, test")
}
`)
helloTestHTML := template.HTML(`
<pre><span num="2">package main</span>
<span num="3"></span>
<span num="4">import &#34;fmt&#34;</span>
<span num="5"></span>
<span num="6">func main() {</span>
<span num="7"> fmt.Println(&#34;hello, test&#34;)</span>
<span num="8">}</span>
</pre>
`)
helloTestHL := []byte(`
package main
import "fmt" // HLimport
func main() { // HLfunc
fmt.Println("hello, test") // HL
}
`)
highlight := func(h template.HTML, s string) template.HTML {
return template.HTML(strings.Replace(string(h), s, "<b>"+s+"</b>", -1))
}
read := func(b []byte, err error) func(string) ([]byte, error) {
return func(string) ([]byte, error) { return b, err }
}
tests := []struct {
name string
readFile func(string) ([]byte, error)
sourceFile string
cmd string
err string
Code
}{
{
name: "all code, no play",
readFile: read(helloTest, nil),
sourceFile: "main.go",
cmd: ".code main.go",
Code: Code{
Ext: ".go",
FileName: "main.go",
Raw: helloTest,
Text: helloTestHTML,
},
},
{
name: "all code, play",
readFile: read(helloTest, nil),
sourceFile: "main.go",
cmd: ".play main.go",
Code: Code{
Ext: ".go",
FileName: "main.go",
Play: true,
Raw: helloTest,
Text: helloTestHTML,
},
},
{
name: "all code, highlighted",
readFile: read(helloTestHL, nil),
sourceFile: "main.go",
cmd: ".code main.go",
Code: Code{
Ext: ".go",
FileName: "main.go",
Raw: helloTestHL,
Text: highlight(helloTestHTML, "fmt.Println(&#34;hello, test&#34;)"),
},
},
{
name: "highlight only func",
readFile: read(helloTestHL, nil),
sourceFile: "main.go",
cmd: ".code main.go HLfunc",
Code: Code{
Ext: ".go",
FileName: "main.go",
Play: false,
Raw: []byte("package main\n\nimport \"fmt\" // HLimport\n\nfunc main() { // HLfunc\n\tfmt.Println(\"hello, test\") // HL\n}"),
Text: highlight(helloTestHTML, "func main() {"),
},
},
{
name: "bad highlight syntax",
readFile: read(helloTest, nil),
sourceFile: "main.go",
cmd: ".code main.go HL",
err: "invalid highlight syntax",
},
{
name: "error reading file",
readFile: read(nil, fmt.Errorf("nope")),
sourceFile: "main.go",
cmd: ".code main.go",
err: "main.go:0: nope",
},
{
name: "from func main to the end",
readFile: read(helloTest, nil),
sourceFile: "main.go",
cmd: ".code main.go /func main/,",
Code: Code{
Ext: ".go",
FileName: "main.go",
Play: false,
Raw: []byte("func main() {\n\tfmt.Println(\"hello, test\")\n}"),
Text: "<pre><span num=\"6\">func main() {</span>\n<span num=\"7\"> fmt.Println(&#34;hello, test&#34;)</span>\n<span num=\"8\">}</span>\n</pre>",
},
},
{
name: "just func main",
readFile: read(helloTest, nil),
sourceFile: "main.go",
cmd: ".code main.go /func main/",
Code: Code{
Ext: ".go",
FileName: "main.go",
Play: false,
Raw: []byte("func main() {"),
Text: "<pre><span num=\"6\">func main() {</span>\n</pre>",
},
},
{
name: "bad address",
readFile: read(helloTest, nil),
sourceFile: "main.go",
cmd: ".code main.go /function main/",
err: "main.go:0: no match for function main",
},
{
name: "all code with numbers",
readFile: read(helloTest, nil),
sourceFile: "main.go",
cmd: ".code -numbers main.go",
Code: Code{
Ext: ".go",
FileName: "main.go",
Raw: helloTest,
// Replacing the first "<pre>"
Text: "<pre class=\"numbers\">" + helloTestHTML[6:],
},
},
{
name: "all code editable",
readFile: read(helloTest, nil),
sourceFile: "main.go",
cmd: ".code -edit main.go",
Code: Code{
Ext: ".go",
FileName: "main.go",
Raw: helloTest,
Text: "<pre contenteditable=\"true\" spellcheck=\"false\">" + helloTestHTML[6:],
},
},
}
trimHTML := func(t template.HTML) string { return strings.TrimSpace(string(t)) }
trimBytes := func(b []byte) string { return strings.TrimSpace(string(b)) }
for _, tt := range tests {
ctx := &Context{tt.readFile}
e, err := parseCode(ctx, tt.sourceFile, 0, tt.cmd)
if err != nil {
if tt.err == "" {
t.Errorf("%s: unexpected error %v", tt.name, err)
} else if !strings.Contains(err.Error(), tt.err) {
t.Errorf("%s: expected error %s; got %v", tt.name, tt.err, err)
}
continue
}
if tt.err != "" {
t.Errorf("%s: expected error %s; but got none", tt.name, tt.err)
continue
}
c, ok := e.(Code)
if !ok {
t.Errorf("%s: expected a Code value; got %T", tt.name, e)
continue
}
if c.FileName != tt.FileName {
t.Errorf("%s: expected FileName %s; got %s", tt.name, tt.FileName, c.FileName)
}
if c.Ext != tt.Ext {
t.Errorf("%s: expected Ext %s; got %s", tt.name, tt.Ext, c.Ext)
}
if c.Play != tt.Play {
t.Errorf("%s: expected Play %v; got %v", tt.name, tt.Play, c.Play)
}
if got, wants := trimBytes(c.Raw), trimBytes(tt.Raw); got != wants {
t.Errorf("%s: expected Raw \n%q\n; got \n%q\n", tt.name, wants, got)
}
if got, wants := trimHTML(c.Text), trimHTML(tt.Text); got != wants {
t.Errorf("%s: expected Text \n%q\n; got \n%q\n", tt.name, wants, got)
}
}
}
@@ -0,0 +1,382 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package present implements parsing and rendering of present files,
which can be slide presentations as in golang.org/x/tools/cmd/present
or articles as in golang.org/x/blog (the Go blog).
# File Format
Present files begin with a header giving the title of the document
and other metadata, which looks like:
# Title of document
Subtitle of document
15:04 2 Jan 2006
Tags: foo, bar, baz
Summary: This is a great document you want to read.
OldURL: former-path-for-this-doc
The "# " prefix before the title indicates that this is
a Markdown-enabled present file: it uses
Markdown for text markup in the body of the file.
If the "# " prefix is missing, the file uses
legacy present markup, described below.
The date line may be written without a time:
2 Jan 2006
In this case, the time will be interpreted as 10am UTC on that date.
The tags line is a comma-separated list of tags that may be used to categorize
the document.
The summary line gives a short summary used in blog feeds.
The old URL line, which may be repeated, gives an older (perhaps relative) URL
for this document.
A server might use these to generate appropriate redirects.
Only the title is required;
the subtitle, date, tags, summary, and old URL lines are optional.
In Markdown-enabled present, the summary defaults to being empty.
In legacy present, the summary defaults to the first paragraph of text.
After the header come zero or more author blocks, like this:
Author Name
Job title, Company
joe@example.com
https://url/
@twitter_name
The first line of the author block is conventionally the author name.
Otherwise, the author section may contain a mixture of text, twitter names, and links.
For slide presentations, only the plain text lines will be displayed on the
first slide.
If multiple author blocks are listed, each new block must be preceded
by its own blank line.
After the author blocks come the presentation slides or article sections,
which can in turn have subsections.
In Markdown-enabled present files, each slide or section begins with a "##" header line,
subsections begin with a "###" header line, and so on.
In legacy present files, each slide or section begins with a "*" header line,
subsections begin with a "**" header line, and so on.
In addition to the marked-up text in a section (or subsection),
a present file can contain present command invocations, each of which begins
with a dot, as in:
.code x.go /^func main/,/^}/
.play y.go
.image image.jpg
.background image.jpg
.iframe https://foo
.link https://foo label
.html file.html
.caption _Gopher_ by [[https://instagram.com/reneefrench][Renee French]]
Other than the commands, the text in a section is interpreted
either as Markdown or as legacy present markup.
# Markdown Syntax
Markdown typically means the generic name for a family of similar markup languages.
The specific variant used in present is CommonMark.
See https://commonmark.org/help/tutorial/ for a quick tutorial.
In Markdown-enabled present,
section headings can end in {#name} to set the HTML anchor ID for the heading to "name".
Lines beginning with "//" (outside of code blocks, of course)
are treated as present comments and have no effect.
Lines beginning with ": " are treated as speaker notes, described below.
Example:
# Title of Talk
My Name
9 Mar 2020
me@example.com
## Title of Slide or Section (must begin with ##)
Some Text
### Subsection {#anchor}
- bullets
- more bullets
- a bullet continued
on the next line
#### Sub-subsection
Some More text
Preformatted text (code block)
is indented (by one tab, or four spaces)
Further Text, including command invocations.
## Section 2: Example formatting {#fmt}
Formatting:
_italic_
// A comment that is completely ignored.
: Speaker notes.
**bold**
`program`
Markup—_especially italic text_—can easily be overused.
_Why use scoped\_ptr_? Use plain **\*ptr** instead.
Visit [the Go home page](https://golang.org/).
# Legacy Present Syntax
Compared to Markdown,
in legacy present
slides/sections use "*" instead of "##",
whole-line comments begin with "#" instead of "//",
bullet lists can only contain single (possibly wrapped) text lines,
and the font styling and link syntaxes are subtly different.
Example:
Title of Talk
My Name
1 Jan 2013
me@example.com
* Title of Slide or Section (must begin with *)
Some Text
** Subsection
- bullets
- more bullets
- a bullet continued
on the next line (indented at least one space)
*** Sub-subsection
Some More text
Preformatted text (code block)
is indented (however you like)
Further Text, including command invocations.
* Section 2: Example formatting
Formatting:
_italic_
*bold*
`program`
Markup—_especially_italic_text_—can easily be overused.
_Why_use_scoped__ptr_? Use plain ***ptr* instead.
Visit [[https://golang.org][the Go home page]].
Within the input for plain text or lists, text bracketed by font
markers will be presented in italic, bold, or program font.
Marker characters are _ (italic), * (bold) and ` (program font).
An opening marker must be preceded by a space or punctuation
character or else be at start of a line; similarly, a closing
marker must be followed by a space or punctuation character or
else be at the end of a line. Unmatched markers appear as plain text.
There must be no spaces between markers. Within marked text,
a single marker character becomes a space and a doubled single
marker quotes the marker character.
Links can be included in any text with either explicit labels
or the URL itself as the label. For example:
[[url][label]]
[[url]]
# Command Invocations
A number of special commands are available through invocations
in the input text. Each such invocation contains a period as the
first character on the line, followed immediately by the name of
the function, followed by any arguments. A typical invocation might
be
.play demo.go /^func show/,/^}/
(except that the ".play" must be at the beginning of the line and
not be indented as in this comment.)
Here follows a description of the functions:
code:
Injects program source into the output by extracting code from files
and injecting them as HTML-escaped <pre> blocks. The argument is
a file name followed by an optional address that specifies what
section of the file to display. The address syntax is similar in
its simplest form to that of ed, but comes from sam and is more
general. See
https://plan9.io/sys/doc/sam/sam.html Table II
for full details. The displayed block is always rounded out to a
full line at both ends.
If no pattern is present, the entire file is displayed.
Any line in the program that ends with the four characters
OMIT
is deleted from the source before inclusion, making it easy
to write things like
.code test.go /START OMIT/,/END OMIT/
to find snippets like this
tedious_code = boring_function()
// START OMIT
interesting_code = fascinating_function()
// END OMIT
and see only this:
interesting_code = fascinating_function()
Also, inside the displayed text a line that ends
// HL
will be highlighted in the display. A highlighting mark may have a
suffix word, such as
// HLxxx
Such highlights are enabled only if the code invocation ends with
"HL" followed by the word:
.code test.go /^type Foo/,/^}/ HLxxx
The .code function may take one or more flags immediately preceding
the filename. This command shows test.go in an editable text area:
.code -edit test.go
This command shows test.go with line numbers:
.code -numbers test.go
play:
The function "play" is the same as "code" but puts a button
on the displayed source so the program can be run from the browser.
Although only the selected text is shown, all the source is included
in the HTML output so it can be presented to the compiler.
link:
Create a hyperlink. The syntax is 1 or 2 space-separated arguments.
The first argument is always the HTTP URL. If there is a second
argument, it is the text label to display for this link.
.link https://golang.org golang.org
image:
The template uses the function "image" to inject picture files.
The syntax is simple: 1 or 3 space-separated arguments.
The first argument is always the file name.
If there are more arguments, they are the height and width;
both must be present, or substituted with an underscore.
Replacing a dimension argument with the underscore parameter
preserves the aspect ratio of the image when scaling.
.image images/betsy.jpg 100 200
.image images/janet.jpg _ 300
video:
The template uses the function "video" to inject video files.
The syntax is simple: 2 or 4 space-separated arguments.
The first argument is always the file name.
The second argument is always the file content-type.
If there are more arguments, they are the height and width;
both must be present, or substituted with an underscore.
Replacing a dimension argument with the underscore parameter
preserves the aspect ratio of the video when scaling.
.video videos/evangeline.mp4 video/mp4 400 600
.video videos/mabel.ogg video/ogg 500 _
background:
The template uses the function "background" to set the background image for
a slide. The only argument is the file name of the image.
.background images/susan.jpg
caption:
The template uses the function "caption" to inject figure captions.
The text after ".caption" is embedded in a figcaption element after
processing styling and links as in standard text lines.
.caption _Gopher_ by [[https://instagram.com/reneefrench][Renee French]]
iframe:
The function "iframe" injects iframes (pages inside pages).
Its syntax is the same as that of image.
html:
The function html includes the contents of the specified file as
unescaped HTML. This is useful for including custom HTML elements
that cannot be created using only the slide format.
It is your responsibility to make sure the included HTML is valid and safe.
.html file.html
# Presenter Notes
Lines that begin with ": " are treated as presenter notes,
in both Markdown and legacy present syntax.
By default, presenter notes are collected but ignored.
When running the present command with -notes,
typing 'N' in your browser displaying your slides
will create a second window displaying the notes.
The second window is completely synced with the main
window, except that presenter notes are only visible in the second window.
Notes may appear anywhere within the slide text. For example:
## Title of slide
Some text.
: Presenter notes (first paragraph)
Some more text.
: Presenter notes (subsequent paragraph(s))
*/
package present // import "golang.org/x/tools/present"
@@ -0,0 +1,37 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package present
import (
"errors"
"html/template"
"path/filepath"
"strings"
)
func init() {
Register("html", parseHTML)
}
func parseHTML(ctx *Context, fileName string, lineno int, text string) (Elem, error) {
p := strings.Fields(text)
if len(p) != 2 {
return nil, errors.New("invalid .html args")
}
name := filepath.Join(filepath.Dir(fileName), p[1])
b, err := ctx.ReadFile(name)
if err != nil {
return nil, err
}
return HTML{text, template.HTML(b)}, nil
}
type HTML struct {
Cmd string // original command from present source
template.HTML
}
func (s HTML) PresentCmd() string { return s.Cmd }
func (s HTML) TemplateName() string { return "html" }
@@ -0,0 +1,50 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package present
import (
"fmt"
"strings"
)
func init() {
Register("iframe", parseIframe)
}
type Iframe struct {
Cmd string // original command from present source
URL string
Width int
Height int
}
func (i Iframe) PresentCmd() string { return i.Cmd }
func (i Iframe) TemplateName() string { return "iframe" }
func parseIframe(ctx *Context, fileName string, lineno int, text string) (Elem, error) {
args := strings.Fields(text)
if len(args) < 2 {
return nil, fmt.Errorf("incorrect iframe invocation: %q", text)
}
i := Iframe{Cmd: text, URL: args[1]}
a, err := parseArgs(fileName, lineno, args[2:])
if err != nil {
return nil, err
}
switch len(a) {
case 0:
// no size parameters
case 2:
if v, ok := a[0].(int); ok {
i.Height = v
}
if v, ok := a[1].(int); ok {
i.Width = v
}
default:
return nil, fmt.Errorf("incorrect iframe invocation: %q", text)
}
return i, nil
}
@@ -0,0 +1,55 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package present
import (
"fmt"
"strings"
)
func init() {
Register("image", parseImage)
}
type Image struct {
Cmd string // original command from present source
URL string
Width int
Height int
}
func (i Image) PresentCmd() string { return i.Cmd }
func (i Image) TemplateName() string { return "image" }
func parseImage(ctx *Context, fileName string, lineno int, text string) (Elem, error) {
args := strings.Fields(text)
if len(args) < 2 {
return nil, fmt.Errorf("incorrect image invocation: %q", text)
}
img := Image{Cmd: text, URL: args[1]}
a, err := parseArgs(fileName, lineno, args[2:])
if err != nil {
return nil, err
}
switch len(a) {
case 0:
// no size parameters
case 2:
// If a parameter is empty (underscore) or invalid
// leave the field set to zero. The "image" action
// template will then omit that img tag attribute and
// the browser will calculate the value to preserve
// the aspect ratio.
if v, ok := a[0].(int); ok {
img.Height = v
}
if v, ok := a[1].(int); ok {
img.Width = v
}
default:
return nil, fmt.Errorf("incorrect image invocation: %q", text)
}
return img, nil
}
@@ -0,0 +1,102 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package present
import (
"fmt"
"log"
"net/url"
"strings"
)
func init() {
Register("link", parseLink)
}
type Link struct {
Cmd string // original command from present source
URL *url.URL
Label string
}
func (l Link) PresentCmd() string { return l.Cmd }
func (l Link) TemplateName() string { return "link" }
func parseLink(ctx *Context, fileName string, lineno int, text string) (Elem, error) {
args := strings.Fields(text)
if len(args) < 2 {
return nil, fmt.Errorf("link element must have at least 2 arguments")
}
url, err := url.Parse(args[1])
if err != nil {
return nil, err
}
label := ""
if len(args) > 2 {
label = strings.Join(args[2:], " ")
} else {
scheme := url.Scheme + "://"
if url.Scheme == "mailto" {
scheme = "mailto:"
}
label = strings.Replace(url.String(), scheme, "", 1)
}
return Link{text, url, label}, nil
}
func renderLink(href, text string) string {
text = font(text)
if text == "" {
text = href
}
// Open links in new window only when their url is absolute.
target := "_blank"
if u, err := url.Parse(href); err != nil {
log.Println("renderLink parsing url:", err)
} else if !u.IsAbs() || u.Scheme == "javascript" {
target = "_self"
}
return fmt.Sprintf(`<a href="%s" target="%s">%s</a>`, href, target, text)
}
// parseInlineLink parses an inline link at the start of s, and returns
// a rendered HTML link and the total length of the raw inline link.
// If no inline link is present, it returns all zeroes.
func parseInlineLink(s string) (link string, length int) {
if !strings.HasPrefix(s, "[[") {
return
}
end := strings.Index(s, "]]")
if end == -1 {
return
}
urlEnd := strings.Index(s, "]")
rawURL := s[2:urlEnd]
const badURLChars = `<>"{}|\^[] ` + "`" // per RFC2396 section 2.4.3
if strings.ContainsAny(rawURL, badURLChars) {
return
}
if urlEnd == end {
simpleURL := ""
url, err := url.Parse(rawURL)
if err == nil {
// If the URL is http://foo.com, drop the http://
// In other words, render [[http://golang.org]] as:
// <a href="http://golang.org">golang.org</a>
if strings.HasPrefix(rawURL, url.Scheme+"://") {
simpleURL = strings.TrimPrefix(rawURL, url.Scheme+"://")
} else if strings.HasPrefix(rawURL, url.Scheme+":") {
simpleURL = strings.TrimPrefix(rawURL, url.Scheme+":")
}
}
return renderLink(rawURL, simpleURL), end + 2
}
if s[urlEnd:urlEnd+2] != "][" {
return
}
text := s[urlEnd+2 : end]
return renderLink(rawURL, text), end + 2
}
@@ -0,0 +1,40 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package present
import "testing"
func TestInlineParsing(t *testing.T) {
var tests = []struct {
in string
link string
text string
length int
}{
{"[[http://golang.org]]", "http://golang.org", "golang.org", 21},
{"[[http://golang.org][]]", "http://golang.org", "http://golang.org", 23},
{"[[http://golang.org]] this is ignored", "http://golang.org", "golang.org", 21},
{"[[http://golang.org][link]]", "http://golang.org", "link", 27},
{"[[http://golang.org][two words]]", "http://golang.org", "two words", 32},
{"[[http://golang.org][*link*]]", "http://golang.org", "<b>link</b>", 29},
{"[[http://bad[url]]", "", "", 0},
{"[[http://golang.org][a [[link]] ]]", "http://golang.org", "a [[link", 31},
{"[[http:// *spaces* .com]]", "", "", 0},
{"[[http://bad`char.com]]", "", "", 0},
{" [[http://google.com]]", "", "", 0},
{"[[mailto:gopher@golang.org][Gopher]]", "mailto:gopher@golang.org", "Gopher", 36},
{"[[mailto:gopher@golang.org]]", "mailto:gopher@golang.org", "gopher@golang.org", 28},
}
for i, test := range tests {
link, length := parseInlineLink(test.in)
if length == 0 && test.length == 0 {
continue
}
if a := renderLink(test.link, test.text); length != test.length || link != a {
t.Errorf("#%d: parseInlineLink(%q):\ngot\t%q, %d\nwant\t%q, %d", i, test.in, link, length, a, test.length)
}
}
}
@@ -0,0 +1,725 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package present
import (
"bufio"
"bytes"
"errors"
"fmt"
"html/template"
"io"
"log"
"net/url"
"os"
"regexp"
"strings"
"time"
"unicode"
"unicode/utf8"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
)
var (
parsers = make(map[string]ParseFunc)
funcs = template.FuncMap{}
)
// Template returns an empty template with the action functions in its FuncMap.
func Template() *template.Template {
return template.New("").Funcs(funcs)
}
// Render renders the doc to the given writer using the provided template.
func (d *Doc) Render(w io.Writer, t *template.Template) error {
data := struct {
*Doc
Template *template.Template
PlayEnabled bool
NotesEnabled bool
}{d, t, PlayEnabled, NotesEnabled}
return t.ExecuteTemplate(w, "root", data)
}
// Render renders the section to the given writer using the provided template.
func (s *Section) Render(w io.Writer, t *template.Template) error {
data := struct {
*Section
Template *template.Template
PlayEnabled bool
}{s, t, PlayEnabled}
return t.ExecuteTemplate(w, "section", data)
}
type ParseFunc func(ctx *Context, fileName string, lineNumber int, inputLine string) (Elem, error)
// Register binds the named action, which does not begin with a period, to the
// specified parser to be invoked when the name, with a period, appears in the
// present input text.
func Register(name string, parser ParseFunc) {
if len(name) == 0 || name[0] == ';' {
panic("bad name in Register: " + name)
}
parsers["."+name] = parser
}
// Doc represents an entire document.
type Doc struct {
Title string
Subtitle string
Summary string
Time time.Time
Authors []Author
TitleNotes []string
Sections []Section
Tags []string
OldURL []string
}
// Author represents the person who wrote and/or is presenting the document.
type Author struct {
Elem []Elem
}
// TextElem returns the first text elements of the author details.
// This is used to display the author' name, job title, and company
// without the contact details.
func (p *Author) TextElem() (elems []Elem) {
for _, el := range p.Elem {
if _, ok := el.(Text); !ok {
break
}
elems = append(elems, el)
}
return
}
// Section represents a section of a document (such as a presentation slide)
// comprising a title and a list of elements.
type Section struct {
Number []int
Title string
ID string // HTML anchor ID
Elem []Elem
Notes []string
Classes []string
Styles []string
}
// HTMLAttributes for the section
func (s Section) HTMLAttributes() template.HTMLAttr {
if len(s.Classes) == 0 && len(s.Styles) == 0 {
return ""
}
var class string
if len(s.Classes) > 0 {
class = fmt.Sprintf(`class=%q`, strings.Join(s.Classes, " "))
}
var style string
if len(s.Styles) > 0 {
style = fmt.Sprintf(`style=%q`, strings.Join(s.Styles, " "))
}
return template.HTMLAttr(strings.Join([]string{class, style}, " "))
}
// Sections contained within the section.
func (s Section) Sections() (sections []Section) {
for _, e := range s.Elem {
if section, ok := e.(Section); ok {
sections = append(sections, section)
}
}
return
}
// Level returns the level of the given section.
// The document title is level 1, main section 2, etc.
func (s Section) Level() int {
return len(s.Number) + 1
}
// FormattedNumber returns a string containing the concatenation of the
// numbers identifying a Section.
func (s Section) FormattedNumber() string {
b := &bytes.Buffer{}
for _, n := range s.Number {
fmt.Fprintf(b, "%v.", n)
}
return b.String()
}
func (s Section) TemplateName() string { return "section" }
// Elem defines the interface for a present element. That is, something that
// can provide the name of the template used to render the element.
type Elem interface {
TemplateName() string
}
// renderElem implements the elem template function, used to render
// sub-templates.
func renderElem(t *template.Template, e Elem) (template.HTML, error) {
var data interface{} = e
if s, ok := e.(Section); ok {
data = struct {
Section
Template *template.Template
}{s, t}
}
return execTemplate(t, e.TemplateName(), data)
}
// pageNum derives a page number from a section.
func pageNum(s Section, offset int) int {
if len(s.Number) == 0 {
return offset
}
return s.Number[0] + offset
}
func init() {
funcs["elem"] = renderElem
funcs["pagenum"] = pageNum
}
// execTemplate is a helper to execute a template and return the output as a
// template.HTML value.
func execTemplate(t *template.Template, name string, data interface{}) (template.HTML, error) {
b := new(bytes.Buffer)
err := t.ExecuteTemplate(b, name, data)
if err != nil {
return "", err
}
return template.HTML(b.String()), nil
}
// Text represents an optionally preformatted paragraph.
type Text struct {
Lines []string
Pre bool
Raw string // original text, for Pre==true
}
func (t Text) TemplateName() string { return "text" }
// List represents a bulleted list.
type List struct {
Bullet []string
}
func (l List) TemplateName() string { return "list" }
// Lines is a helper for parsing line-based input.
type Lines struct {
line int // 0 indexed, so has 1-indexed number of last line returned
text []string
comment string
}
func readLines(r io.Reader) (*Lines, error) {
var lines []string
s := bufio.NewScanner(r)
for s.Scan() {
lines = append(lines, s.Text())
}
if err := s.Err(); err != nil {
return nil, err
}
return &Lines{0, lines, "#"}, nil
}
func (l *Lines) next() (text string, ok bool) {
for {
current := l.line
l.line++
if current >= len(l.text) {
return "", false
}
text = l.text[current]
// Lines starting with l.comment are comments.
if l.comment == "" || !strings.HasPrefix(text, l.comment) {
ok = true
break
}
}
return
}
func (l *Lines) back() {
l.line--
}
func (l *Lines) nextNonEmpty() (text string, ok bool) {
for {
text, ok = l.next()
if !ok {
return
}
if len(text) > 0 {
break
}
}
return
}
// A Context specifies the supporting context for parsing a presentation.
type Context struct {
// ReadFile reads the file named by filename and returns the contents.
ReadFile func(filename string) ([]byte, error)
}
// ParseMode represents flags for the Parse function.
type ParseMode int
const (
// If set, parse only the title and subtitle.
TitlesOnly ParseMode = 1
)
// Parse parses a document from r.
func (ctx *Context) Parse(r io.Reader, name string, mode ParseMode) (*Doc, error) {
doc := new(Doc)
lines, err := readLines(r)
if err != nil {
return nil, err
}
// Detect Markdown-enabled vs legacy present file.
// Markdown-enabled files have a title line beginning with "# "
// (like preprocessed C files of yore).
isMarkdown := false
for i := lines.line; i < len(lines.text); i++ {
line := lines.text[i]
if line == "" {
continue
}
isMarkdown = strings.HasPrefix(line, "# ")
break
}
sectionPrefix := "*"
if isMarkdown {
sectionPrefix = "##"
lines.comment = "//"
}
for i := lines.line; i < len(lines.text); i++ {
if strings.HasPrefix(lines.text[i], sectionPrefix) {
break
}
if isSpeakerNote(lines.text[i]) {
doc.TitleNotes = append(doc.TitleNotes, trimSpeakerNote(lines.text[i]))
}
}
err = parseHeader(doc, isMarkdown, lines)
if err != nil {
return nil, err
}
if mode&TitlesOnly != 0 {
return doc, nil
}
// Authors
if doc.Authors, err = parseAuthors(name, sectionPrefix, lines); err != nil {
return nil, err
}
// Sections
if doc.Sections, err = parseSections(ctx, name, sectionPrefix, lines, []int{}); err != nil {
return nil, err
}
return doc, nil
}
// Parse parses a document from r. Parse reads assets used by the presentation
// from the file system using os.ReadFile.
func Parse(r io.Reader, name string, mode ParseMode) (*Doc, error) {
ctx := Context{ReadFile: os.ReadFile}
return ctx.Parse(r, name, mode)
}
// isHeading matches any section heading.
var (
isHeadingLegacy = regexp.MustCompile(`^\*+( |$)`)
isHeadingMarkdown = regexp.MustCompile(`^\#+( |$)`)
)
// lesserHeading returns true if text is a heading of a lesser or equal level
// than that denoted by prefix.
func lesserHeading(isHeading *regexp.Regexp, text, prefix string) bool {
return isHeading.MatchString(text) && !strings.HasPrefix(text, prefix+prefix[:1])
}
// parseSections parses Sections from lines for the section level indicated by
// number (a nil number indicates the top level).
func parseSections(ctx *Context, name, prefix string, lines *Lines, number []int) ([]Section, error) {
isMarkdown := prefix[0] == '#'
isHeading := isHeadingLegacy
if isMarkdown {
isHeading = isHeadingMarkdown
}
var sections []Section
for i := 1; ; i++ {
// Next non-empty line is title.
text, ok := lines.nextNonEmpty()
for ok && text == "" {
text, ok = lines.next()
}
if !ok {
break
}
if text != prefix && !strings.HasPrefix(text, prefix+" ") {
lines.back()
break
}
// Markdown sections can end in {#id} to set the HTML anchor for the section.
// This is nicer than the default #TOC_1_2-style anchor.
title := strings.TrimSpace(text[len(prefix):])
id := ""
if isMarkdown && strings.HasSuffix(title, "}") {
j := strings.LastIndex(title, "{#")
if j >= 0 {
id = title[j+2 : len(title)-1]
title = strings.TrimSpace(title[:j])
}
}
section := Section{
Number: append(append([]int{}, number...), i),
Title: title,
ID: id,
}
text, ok = lines.nextNonEmpty()
for ok && !lesserHeading(isHeading, text, prefix) {
var e Elem
r, _ := utf8.DecodeRuneInString(text)
switch {
case !isMarkdown && unicode.IsSpace(r):
i := strings.IndexFunc(text, func(r rune) bool {
return !unicode.IsSpace(r)
})
if i < 0 {
break
}
indent := text[:i]
var s []string
for ok && (strings.HasPrefix(text, indent) || text == "") {
if text != "" {
text = text[i:]
}
s = append(s, text)
text, ok = lines.next()
}
lines.back()
pre := strings.Join(s, "\n")
raw := pre
pre = strings.Replace(pre, "\t", " ", -1) // browsers treat tabs badly
pre = strings.TrimRightFunc(pre, unicode.IsSpace)
e = Text{Lines: []string{pre}, Pre: true, Raw: raw}
case !isMarkdown && strings.HasPrefix(text, "- "):
var b []string
for {
if strings.HasPrefix(text, "- ") {
b = append(b, text[2:])
} else if len(b) > 0 && strings.HasPrefix(text, " ") {
b[len(b)-1] += "\n" + strings.TrimSpace(text)
} else {
break
}
if text, ok = lines.next(); !ok {
break
}
}
lines.back()
e = List{Bullet: b}
case isSpeakerNote(text):
section.Notes = append(section.Notes, trimSpeakerNote(text))
case strings.HasPrefix(text, prefix+prefix[:1]+" ") || text == prefix+prefix[:1]:
lines.back()
subsecs, err := parseSections(ctx, name, prefix+prefix[:1], lines, section.Number)
if err != nil {
return nil, err
}
for _, ss := range subsecs {
section.Elem = append(section.Elem, ss)
}
case strings.HasPrefix(text, prefix+prefix[:1]):
return nil, fmt.Errorf("%s:%d: badly nested section inside %s: %s", name, lines.line, prefix, text)
case strings.HasPrefix(text, "."):
args := strings.Fields(text)
if args[0] == ".background" {
section.Classes = append(section.Classes, "background")
section.Styles = append(section.Styles, "background-image: url('"+args[1]+"')")
break
}
parser := parsers[args[0]]
if parser == nil {
return nil, fmt.Errorf("%s:%d: unknown command %q", name, lines.line, text)
}
t, err := parser(ctx, name, lines.line, text)
if err != nil {
return nil, err
}
e = t
case isMarkdown:
// Collect Markdown lines, including blank lines and indented text.
var block []string
endLine, endBlock := lines.line-1, -1 // end is last non-empty line
for ok {
trim := strings.TrimSpace(text)
if trim != "" {
// Command breaks text block.
// Section heading breaks text block in markdown.
if text[0] == '.' || text[0] == '#' || isSpeakerNote(text) {
break
}
if strings.HasPrefix(text, `\.`) { // Backslash escapes initial period.
text = text[1:]
}
endLine, endBlock = lines.line, len(block)
}
block = append(block, text)
text, ok = lines.next()
}
block = block[:endBlock+1]
lines.line = endLine + 1
if len(block) == 0 {
break
}
// Replace all leading tabs with 4 spaces,
// which render better in code blocks.
// CommonMark defines that for parsing the structure of the file
// a tab is equivalent to 4 spaces, so this change won't
// affect the later parsing at all.
// An alternative would be to apply this to code blocks after parsing,
// at the same time that we update <a> targets, but that turns out
// to be quite difficult to modify in the AST.
for i, line := range block {
if len(line) > 0 && line[0] == '\t' {
short := strings.TrimLeft(line, "\t")
line = strings.Repeat(" ", len(line)-len(short)) + short
block[i] = line
}
}
html, err := renderMarkdown([]byte(strings.Join(block, "\n")))
if err != nil {
return nil, err
}
e = HTML{HTML: html}
default:
// Collect text lines.
var block []string
for ok && strings.TrimSpace(text) != "" {
// Command breaks text block.
// Section heading breaks text block in markdown.
if text[0] == '.' || isSpeakerNote(text) {
lines.back()
break
}
if strings.HasPrefix(text, `\.`) { // Backslash escapes initial period.
text = text[1:]
}
block = append(block, text)
text, ok = lines.next()
}
if len(block) == 0 {
break
}
e = Text{Lines: block}
}
if e != nil {
section.Elem = append(section.Elem, e)
}
text, ok = lines.nextNonEmpty()
}
if isHeading.MatchString(text) {
lines.back()
}
sections = append(sections, section)
}
if len(sections) == 0 {
return nil, fmt.Errorf("%s:%d: unexpected line: %s", name, lines.line+1, lines.text[lines.line])
}
return sections, nil
}
func parseHeader(doc *Doc, isMarkdown bool, lines *Lines) error {
var ok bool
// First non-empty line starts header.
doc.Title, ok = lines.nextNonEmpty()
if !ok {
return errors.New("unexpected EOF; expected title")
}
if isMarkdown {
doc.Title = strings.TrimSpace(strings.TrimPrefix(doc.Title, "#"))
}
for {
text, ok := lines.next()
if !ok {
return errors.New("unexpected EOF")
}
if text == "" {
break
}
if isSpeakerNote(text) {
continue
}
if strings.HasPrefix(text, "Tags:") {
tags := strings.Split(text[len("Tags:"):], ",")
for i := range tags {
tags[i] = strings.TrimSpace(tags[i])
}
doc.Tags = append(doc.Tags, tags...)
} else if strings.HasPrefix(text, "Summary:") {
doc.Summary = strings.TrimSpace(text[len("Summary:"):])
} else if strings.HasPrefix(text, "OldURL:") {
doc.OldURL = append(doc.OldURL, strings.TrimSpace(text[len("OldURL:"):]))
} else if t, ok := parseTime(text); ok {
doc.Time = t
} else if doc.Subtitle == "" {
doc.Subtitle = text
} else {
return fmt.Errorf("unexpected header line: %q", text)
}
}
return nil
}
func parseAuthors(name, sectionPrefix string, lines *Lines) (authors []Author, err error) {
// This grammar demarcates authors with blanks.
// Skip blank lines.
if _, ok := lines.nextNonEmpty(); !ok {
return nil, errors.New("unexpected EOF")
}
lines.back()
var a *Author
for {
text, ok := lines.next()
if !ok {
return nil, errors.New("unexpected EOF")
}
// If we find a section heading, we're done.
if strings.HasPrefix(text, sectionPrefix) {
lines.back()
break
}
if isSpeakerNote(text) {
continue
}
// If we encounter a blank we're done with this author.
if a != nil && len(text) == 0 {
authors = append(authors, *a)
a = nil
continue
}
if a == nil {
a = new(Author)
}
// Parse the line. Those that
// - begin with @ are twitter names,
// - contain slashes are links, or
// - contain an @ symbol are an email address.
// The rest is just text.
var el Elem
switch {
case strings.HasPrefix(text, "@"):
el = parseAuthorURL(name, "http://twitter.com/"+text[1:])
case strings.Contains(text, ":"):
el = parseAuthorURL(name, text)
case strings.Contains(text, "@"):
el = parseAuthorURL(name, "mailto:"+text)
}
if l, ok := el.(Link); ok {
l.Label = text
el = l
}
if el == nil {
el = Text{Lines: []string{text}}
}
a.Elem = append(a.Elem, el)
}
if a != nil {
authors = append(authors, *a)
}
return authors, nil
}
func parseAuthorURL(name, text string) Elem {
u, err := url.Parse(text)
if err != nil {
log.Printf("parsing %s author block: invalid URL %q: %v", name, text, err)
return nil
}
return Link{URL: u}
}
func parseTime(text string) (t time.Time, ok bool) {
t, err := time.Parse("15:04 2 Jan 2006", text)
if err == nil {
return t, true
}
t, err = time.Parse("2 Jan 2006", text)
if err == nil {
// at 11am UTC it is the same date everywhere
t = t.Add(time.Hour * 11)
return t, true
}
return time.Time{}, false
}
func isSpeakerNote(s string) bool {
return strings.HasPrefix(s, ": ") || s == ":"
}
func trimSpeakerNote(s string) string {
if s == ":" {
return ""
}
return strings.TrimPrefix(s, ": ")
}
func renderMarkdown(input []byte) (template.HTML, error) {
md := goldmark.New(goldmark.WithRendererOptions(html.WithUnsafe()))
reader := text.NewReader(input)
doc := md.Parser().Parse(reader)
fixupMarkdown(doc)
var b strings.Builder
if err := md.Renderer().Render(&b, input, doc); err != nil {
return "", err
}
return template.HTML(b.String()), nil
}
func fixupMarkdown(n ast.Node) {
ast.Walk(n, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if entering {
switch n := n.(type) {
case *ast.Link:
n.SetAttributeString("target", []byte("_blank"))
// https://developers.google.com/web/tools/lighthouse/audits/noopener
n.SetAttributeString("rel", []byte("noopener"))
}
}
return ast.WalkContinue, nil
})
}
@@ -0,0 +1,173 @@
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package present
import (
"bytes"
"html/template"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"
)
func TestTestdata(t *testing.T) {
tmpl := template.Must(Template().Parse(testTmpl))
filesP, err := filepath.Glob("testdata/*.p")
if err != nil {
t.Fatal(err)
}
filesMD, err := filepath.Glob("testdata/*.md")
if err != nil {
t.Fatal(err)
}
files := append(filesP, filesMD...)
for _, file := range files {
file := file
name := filepath.Base(file)
if name == "README" {
continue
}
t.Run(name, func(t *testing.T) {
data, err := os.ReadFile(file)
if err != nil {
t.Fatalf("%s: %v", file, err)
}
marker := []byte("\n---\n")
i := bytes.Index(data, marker)
if i < 0 {
t.Fatalf("%s: cannot find --- marker in input", file)
}
input, html := data[:i+1], data[i+len(marker):]
doc, err := Parse(bytes.NewReader(input), name, 0)
if err != nil {
t.Fatalf("%s: %v", file, err)
}
var buf bytes.Buffer
if err := doc.Render(&buf, tmpl); err != nil {
t.Fatalf("%s: %v", file, err)
}
if !bytes.Equal(buf.Bytes(), html) {
diffText, err := diff("present-test-", "want", html, "have", buf.Bytes())
if err != nil {
t.Fatalf("%s: diff: %v", file, err)
}
t.Errorf("%s: incorrect html:\n%s", file, diffText)
}
})
}
}
func diff(prefix string, name1 string, b1 []byte, name2 string, b2 []byte) ([]byte, error) {
f1, err := writeTempFile(prefix, b1)
if err != nil {
return nil, err
}
defer os.Remove(f1)
f2, err := writeTempFile(prefix, b2)
if err != nil {
return nil, err
}
defer os.Remove(f2)
cmd := "diff"
if runtime.GOOS == "plan9" {
cmd = "/bin/ape/diff"
}
data, err := exec.Command(cmd, "-u", f1, f2).CombinedOutput()
if len(data) > 0 {
// diff exits with a non-zero status when the files don't match.
// Ignore that failure as long as we get output.
err = nil
}
data = bytes.Replace(data, []byte(f1), []byte(name1), -1)
data = bytes.Replace(data, []byte(f2), []byte(name2), -1)
return data, err
}
func writeTempFile(prefix string, data []byte) (string, error) {
file, err := os.CreateTemp("", prefix)
if err != nil {
return "", err
}
_, err = file.Write(data)
if err1 := file.Close(); err == nil {
err = err1
}
if err != nil {
os.Remove(file.Name())
return "", err
}
return file.Name(), nil
}
var testTmpl = `
{{define "root" -}}
<h1>{{.Title}}</h1>
{{with .Subtitle}}<h2>{{.}}</h2>
{{end -}}
{{range .Authors}}<author>
{{range .Elem}}{{elem $.Template .}}{{end}}</author>
{{end -}}
{{range .Sections}}<section>{{elem $.Template .}}</section>
{{end -}}
{{end}}
{{define "newline"}}{{/* No automatic line break. Paragraphs are free-form. */}}
{{end}}
{{define "section"}}
{{if .Title}}<h2 id="TOC_{{.FormattedNumber}}">{{.Title}}</h2>
{{end -}}
{{range .Elem}}{{elem $.Template .}}{{end}}
{{- end}}
{{define "list" -}}
<ul>
{{range .Bullet -}}
<li>{{style .}}</li>
{{end -}}
</ul>
{{end}}
{{define "text" -}}
{{if .Pre -}}
<pre>{{range .Lines}}{{.}}{{end}}</pre>
{{else -}}
<p>{{range $i, $l := .Lines}}{{if $i}}{{template "newline"}}{{end}}{{style $l}}{{end}}</p>
{{end -}}
{{end}}
{{define "code" -}}
{{if .Play -}}
<div class="playground">{{.Text}}</div>
{{else -}}
<div class="code">{{.Text}}</div>
{{end -}}
{{end}}
{{define "image" -}}
<img src="{{.URL}}"{{with .Height}} height="{{.}}"{{end}}{{with .Width}} width="{{.}}"{{end}} alt="">
{{end}}
{{define "caption" -}}
<figcaption>{{style .Text}}</figcaption>
{{end}}
{{define "iframe" -}}
<iframe src="{{.URL}}"{{with .Height}} height="{{.}}"{{end}}{{with .Width}} width="{{.}}"{{end}}></iframe>
{{end}}
{{define "link" -}}
<p class="link"><a href="{{.URL}}">{{style .Label}}</a></p>
{{end}}
{{define "html" -}}{{.HTML}}{{end}}
`
@@ -0,0 +1,167 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package present
import (
"bytes"
"html"
"html/template"
"strings"
"unicode"
"unicode/utf8"
)
/*
Fonts are demarcated by an initial and final char bracketing a
space-delimited word, plus possibly some terminal punctuation.
The chars are
_ for italic
* for bold
` (back quote) for fixed width.
Inner appearances of the char become spaces. For instance,
_this_is_italic_!
becomes
<i>this is italic</i>!
*/
func init() {
funcs["style"] = Style
}
// Style returns s with HTML entities escaped and font indicators turned into
// HTML font tags.
func Style(s string) template.HTML {
return template.HTML(font(html.EscapeString(s)))
}
// font returns s with font indicators turned into HTML font tags.
func font(s string) string {
if !strings.ContainsAny(s, "[`_*") {
return s
}
words := split(s)
var b bytes.Buffer
Word:
for w, word := range words {
if len(word) < 2 {
continue Word
}
if link, _ := parseInlineLink(word); link != "" {
words[w] = link
continue Word
}
const marker = "_*`"
// Initial punctuation is OK but must be peeled off.
first := strings.IndexAny(word, marker)
if first == -1 {
continue Word
}
// Opening marker must be at the beginning of the token or else preceded by punctuation.
if first != 0 {
r, _ := utf8.DecodeLastRuneInString(word[:first])
if !unicode.IsPunct(r) {
continue Word
}
}
open, word := word[:first], word[first:]
char := word[0] // ASCII is OK.
close := ""
switch char {
default:
continue Word
case '_':
open += "<i>"
close = "</i>"
case '*':
open += "<b>"
close = "</b>"
case '`':
open += "<code>"
close = "</code>"
}
// Closing marker must be at the end of the token or else followed by punctuation.
last := strings.LastIndex(word, word[:1])
if last == 0 {
continue Word
}
if last+1 != len(word) {
r, _ := utf8.DecodeRuneInString(word[last+1:])
if !unicode.IsPunct(r) {
continue Word
}
}
head, tail := word[:last+1], word[last+1:]
b.Reset()
b.WriteString(open)
var wid int
for i := 1; i < len(head)-1; i += wid {
var r rune
r, wid = utf8.DecodeRuneInString(head[i:])
if r != rune(char) {
// Ordinary character.
b.WriteRune(r)
continue
}
if head[i+1] != char {
// Inner char becomes space.
b.WriteRune(' ')
continue
}
// Doubled char becomes real char.
// Not worth worrying about "_x__".
b.WriteByte(char)
wid++ // Consumed two chars, both ASCII.
}
b.WriteString(close) // Write closing tag.
b.WriteString(tail) // Restore trailing punctuation.
words[w] = b.String()
}
return strings.Join(words, "")
}
// split is like strings.Fields but also returns the runs of spaces
// and treats inline links as distinct words.
func split(s string) []string {
var (
words = make([]string, 0, 10)
start = 0
)
// appendWord appends the string s[start:end] to the words slice.
// If the word contains the beginning of a link, the non-link portion
// of the word and the entire link are appended as separate words,
// and the start index is advanced to the end of the link.
appendWord := func(end int) {
if j := strings.Index(s[start:end], "[["); j > -1 {
if _, l := parseInlineLink(s[start+j:]); l > 0 {
// Append portion before link, if any.
if j > 0 {
words = append(words, s[start:start+j])
}
// Append link itself.
words = append(words, s[start+j:start+j+l])
// Advance start index to end of link.
start = start + j + l
return
}
}
// No link; just add the word.
words = append(words, s[start:end])
start = end
}
wasSpace := false
for i, r := range s {
isSpace := unicode.IsSpace(r)
if i > start && isSpace != wasSpace {
appendWord(i)
}
wasSpace = isSpace
}
for start < len(s) {
appendWord(len(s))
}
return words
}
@@ -0,0 +1,124 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package present
import (
"fmt"
"reflect"
"testing"
)
func TestSplit(t *testing.T) {
var tests = []struct {
in string
out []string
}{
{"", []string{}},
{" ", []string{" "}},
{"abc", []string{"abc"}},
{"abc def", []string{"abc", " ", "def"}},
{"abc def ", []string{"abc", " ", "def", " "}},
{"hey [[http://golang.org][Gophers]] around",
[]string{"hey", " ", "[[http://golang.org][Gophers]]", " ", "around"}},
{"A [[http://golang.org/doc][two words]] link",
[]string{"A", " ", "[[http://golang.org/doc][two words]]", " ", "link"}},
{"Visit [[http://golang.org/doc]] now",
[]string{"Visit", " ", "[[http://golang.org/doc]]", " ", "now"}},
{"not [[http://golang.org/doc][a [[link]] ]] around",
[]string{"not", " ", "[[http://golang.org/doc][a [[link]]", " ", "]]", " ", "around"}},
{"[[http://golang.org][foo bar]]",
[]string{"[[http://golang.org][foo bar]]"}},
{"ends with [[http://golang.org][link]]",
[]string{"ends", " ", "with", " ", "[[http://golang.org][link]]"}},
{"my talk ([[http://talks.golang.org/][slides here]])",
[]string{"my", " ", "talk", " ", "(", "[[http://talks.golang.org/][slides here]]", ")"}},
}
for _, test := range tests {
out := split(test.in)
if !reflect.DeepEqual(out, test.out) {
t.Errorf("split(%q):\ngot\t%q\nwant\t%q", test.in, out, test.out)
}
}
}
func TestFont(t *testing.T) {
var tests = []struct {
in string
out string
}{
{"", ""},
{" ", " "},
{"\tx", "\tx"},
{"_a_", "<i>a</i>"},
{"*a*", "<b>a</b>"},
{"`a`", "<code>a</code>"},
{"_a_b_", "<i>a b</i>"},
{"_a__b_", "<i>a_b</i>"},
{"_a___b_", "<i>a_ b</i>"},
{"*a**b*?", "<b>a*b</b>?"},
{"_a_<>_b_.", "<i>a <> b</i>."},
{"(_a_)", "(<i>a</i>)"},
{"((_a_), _b_, _c_).", "((<i>a</i>), <i>b</i>, <i>c</i>)."},
{"(_a)", "(_a)"},
{"(_a)", "(_a)"},
{"_Why_use_scoped__ptr_? Use plain ***ptr* instead.", "<i>Why use scoped_ptr</i>? Use plain <b>*ptr</b> instead."},
{"_hey_ [[http://golang.org][*Gophers*]] *around*",
`<i>hey</i> <a href="http://golang.org" target="_blank"><b>Gophers</b></a> <b>around</b>`},
{"_hey_ [[http://golang.org][so _many_ *Gophers*]] *around*",
`<i>hey</i> <a href="http://golang.org" target="_blank">so <i>many</i> <b>Gophers</b></a> <b>around</b>`},
{"Visit [[http://golang.org]] now",
`Visit <a href="http://golang.org" target="_blank">golang.org</a> now`},
{"my talk ([[http://talks.golang.org/][slides here]])",
`my talk (<a href="http://talks.golang.org/" target="_blank">slides here</a>)`},
{"Markup—_especially_italic_text_—can easily be overused.",
`Markup—<i>especially italic text</i>—can easily be overused.`},
{"`go`get`'s codebase", // ascii U+0027 ' before s
`<code>go get</code>'s codebase`},
{"`go`get`s codebase", // unicode right single quote U+2019 before s
`<code>go get</code>s codebase`},
{"a_variable_name",
`a_variable_name`},
}
for _, test := range tests {
out := font(test.in)
if out != test.out {
t.Errorf("font(%q):\ngot\t%q\nwant\t%q", test.in, out, test.out)
}
}
}
func TestStyle(t *testing.T) {
var tests = []struct {
in string
out string
}{
{"", ""},
{" ", " "},
{"\tx", "\tx"},
{"_a_", "<i>a</i>"},
{"*a*", "<b>a</b>"},
{"`a`", "<code>a</code>"},
{"_a_b_", "<i>a b</i>"},
{"_a__b_", "<i>a_b</i>"},
{"_a___b_", "<i>a_ b</i>"},
{"*a**b*?", "<b>a*b</b>?"},
{"_a_<>_b_.", "<i>a &lt;&gt; b</i>."},
{"(_a_<>_b_)", "(<i>a &lt;&gt; b</i>)"},
{"((_a_), _b_, _c_).", "((<i>a</i>), <i>b</i>, <i>c</i>)."},
{"(_a)", "(_a)"},
}
for _, test := range tests {
out := string(Style(test.in))
if out != test.out {
t.Errorf("style(%q):\ngot\t%q\nwant\t%q", test.in, out, test.out)
}
}
}
func ExampleStyle() {
const s = "*Gophers* are _clearly_ > *cats*!"
fmt.Println(Style(s))
// Output: <b>Gophers</b> are <i>clearly</i> &gt; <b>cats</b>!
}
@@ -0,0 +1,10 @@
This directory contains test data for present's TestTestdata.
Each file named *.p or *.md is expected to contain a present
article, then a --- line, then the HTML output for that article.
By convention, testdata files with a .p extension use legacy present
syntax, while testdata file with a .md extension use Markdown.
This is only a convention: the Markdown syntax must still begi
with "# " to be recognized as Markdown by the present parser.
Other files in this directory are supporting files for the articles.
@@ -0,0 +1,24 @@
# Title
Subtitle
Name
## Heading
Text
on two lines.
More text.
---
<h1>Title</h1>
<h2>Subtitle</h2>
<author>
<p>Name</p>
</author>
<section>
<h2 id="TOC_1.">Heading</h2>
<p>Text
on two lines.</p>
<p>More text.</p>
</section>
@@ -0,0 +1,24 @@
Title
Subtitle
Name
* Heading
Text
on two lines.
More text.
---
<h1>Title</h1>
<h2>Subtitle</h2>
<author>
<p>Name</p>
</author>
<section>
<h2 id="TOC_1.">Heading</h2>
<p>Text
on two lines.</p>
<p>More text.</p>
</section>
@@ -0,0 +1,39 @@
# Code
##
Code:
.code testdata/code.txt
Snippet:
.code testdata/code.txt /Snippet/
Highlight:
.code testdata/code.txt HL1
---
<h1>Code</h1>
<section>
<p>Code:</p>
<div class="code">
<pre><span num="1">code file</span>
<span num="2">Snippet</span>
<span num="3">important</span>
</pre>
</div>
<p>Snippet:</p>
<div class="code">
<pre><span num="2">Snippet</span>
</pre>
</div>
<p>Highlight:</p>
<div class="code">
<pre><span num="1">code file</span>
<span num="2">Snippet</span>
<span num="3"><b>important</b></span>
</pre>
</div>
</section>
@@ -0,0 +1,39 @@
Code
*
Code:
.code testdata/code.txt
Snippet:
.code testdata/code.txt /Snippet/
Highlight:
.code testdata/code.txt HL1
---
<h1>Code</h1>
<section>
<p>Code:</p>
<div class="code">
<pre><span num="1">code file</span>
<span num="2">Snippet</span>
<span num="3">important</span>
</pre>
</div>
<p>Snippet:</p>
<div class="code">
<pre><span num="2">Snippet</span>
</pre>
</div>
<p>Highlight:</p>
<div class="code">
<pre><span num="1">code file</span>
<span num="2">Snippet</span>
<span num="3"><b>important</b></span>
</pre>
</div>
</section>
@@ -0,0 +1,3 @@
code file
Snippet
important // HL1
@@ -0,0 +1,34 @@
# List
##
- Item 1
on two lines.
- Item 2.
- Item 3.
- Item 4 in list despite preceding blank line.
- Item 5.
---
<h1>List</h1>
<section>
<ul>
<li>
<p>Item 1
on two lines.</p>
</li>
<li>
<p>Item 2.</p>
</li>
<li>
<p>Item 3.</p>
</li>
<li>
<p>Item 4 in list despite preceding blank line.</p>
</li>
<li>
<p>Item 5.</p>
</li>
</ul>
</section>
@@ -0,0 +1,26 @@
List
*
- Item 1
on two lines.
- Item 2.
- Item 3.
- Item 1 in new list.
- Item 2.
---
<h1>List</h1>
<section>
<ul>
<li>Item 1
on two lines.</li>
<li>Item 2.</li>
<li>Item 3.</li>
</ul>
<ul>
<li>Item 1 in new list.</li>
<li>Item 2.</li>
</ul>
</section>
@@ -0,0 +1 @@
<!-- media.html -->
@@ -0,0 +1,22 @@
# Media
##
.image gopher.jpg _ 100
.caption A gopher.
.iframe https://golang.org/
.link https://golang.org/ The Gopher's home.
.html testdata/media.html
---
<h1>Media</h1>
<section>
<img src="gopher.jpg" width="100" alt="">
<figcaption>A gopher.</figcaption>
<iframe src="https://golang.org/"></iframe>
<p class="link"><a href="https://golang.org/">The Gopher&#39;s home.</a></p>
<!-- media.html -->
</section>
@@ -0,0 +1,24 @@
Media
*
The Gopher
.image gopher.jpg _ 100
.caption A gopher.
.iframe https://golang.org/
.link https://golang.org/ The Gopher's home.
.html testdata/media.html
---
<h1>Media</h1>
<section>
<p>The Gopher</p>
<img src="gopher.jpg" width="100" alt="">
<figcaption>A gopher.</figcaption>
<iframe src="https://golang.org/"></iframe>
<p class="link"><a href="https://golang.org/">The Gopher&#39;s home.</a></p>
<!-- media.html -->
</section>
@@ -0,0 +1,39 @@
# Pre
##
Pre with tab:
code block
on two lines
Text with space:
now a text block
on two lines
Pre with blank line:
before
after
EOF
---
<h1>Pre</h1>
<section>
<p>Pre with tab:</p>
<pre><code>code block
on two lines
</code></pre>
<p>Text with space:</p>
<p>now a text block
on two lines</p>
<p>Pre with blank line:</p>
<pre><code>before
after
</code></pre>
<p>EOF</p>
</section>
@@ -0,0 +1,37 @@
Pre
*
Pre with tab:
code block
on two lines
Pre with space:
code block
on two lines
Pre with blank line:
before
after
EOF
---
<h1>Pre</h1>
<section>
<p>Pre with tab:</p>
<pre>code block
on two lines</pre>
<p>Pre with space:</p>
<pre>code block
on two lines</pre>
<p>Pre with blank line:</p>
<pre>before
after</pre>
<p>EOF</p>
</section>
@@ -0,0 +1,56 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package present
import (
"fmt"
"strings"
)
func init() {
Register("video", parseVideo)
}
type Video struct {
Cmd string // original command from present source
URL string
SourceType string
Width int
Height int
}
func (v Video) PresentCmd() string { return v.Cmd }
func (v Video) TemplateName() string { return "video" }
func parseVideo(ctx *Context, fileName string, lineno int, text string) (Elem, error) {
args := strings.Fields(text)
if len(args) < 3 {
return nil, fmt.Errorf("incorrect video invocation: %q", text)
}
vid := Video{Cmd: text, URL: args[1], SourceType: args[2]}
a, err := parseArgs(fileName, lineno, args[3:])
if err != nil {
return nil, err
}
switch len(a) {
case 0:
// no size parameters
case 2:
// If a parameter is empty (underscore) or invalid
// leave the field set to zero. The "video" action
// template will then omit that vid tag attribute and
// the browser will calculate the value to preserve
// the aspect ratio.
if v, ok := a[0].(int); ok {
vid.Height = v
}
if v, ok := a[1].(int); ok {
vid.Width = v
}
default:
return nil, fmt.Errorf("incorrect video invocation: %q", text)
}
return vid, nil
}