go.talks: Adding codelab generation to go.talks.
R=adg
CC=golang-dev
https://golang.org/cl/6671043
diff --git a/present/dir.go b/present/dir.go
index 52988d6..f44ac55 100644
--- a/present/dir.go
+++ b/present/dir.go
@@ -28,8 +28,8 @@
}
const base = "."
name := filepath.Join(base, r.URL.Path)
- if filepath.Ext(name) == ".slide" {
- err := renderSlides(w, basePath, name)
+ if isDoc(name) {
+ err := renderDoc(w, basePath, name)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), 500)
@@ -78,7 +78,7 @@
d.Dirs = append(d.Dirs, e)
continue
}
- if filepath.Ext(e.Name) == ".slide" {
+ if isDoc(e.Name) {
if p, err := parse(e.Path, titlesOnly); err != nil {
log.Println(err)
} else {
@@ -102,11 +102,10 @@
func showFile(n string) bool {
switch filepath.Ext(n) {
case ".pdf":
- case ".slide":
case ".html":
case ".go":
default:
- return false
+ return isDoc(n)
}
return true
}
diff --git a/present/doc.go b/present/doc.go
index 76b8497..bf43521 100644
--- a/present/doc.go
+++ b/present/doc.go
@@ -3,11 +3,11 @@
// license that can be found in the LICENSE file.
/*
-Present displays slide presentations.
-It runs a web server that presents slide files from the current directory.
+Present displays slide presentations and articles. It runs a web server that
+presents slide and article files from the current directory.
It may be run as a stand-alone command or an App Engine app.
-The stand-alone version permits the execution of programs from within a slide
+The stand-alone version permits the execution of programs from within a
presentation. The App Engine version does not provide this functionality.
Usage of present:
@@ -19,22 +19,23 @@
to deploy present to App Engine:
appcfg.py update -A your-app-id -V your-app-version /path/to/go.talks
-The file slide format
+The source file format
-Slide files have the following format. The first non-blank non-comment
+Source files have the following format. The first non-blank non-comment
line is the title, so the header looks like
- Title of presentation
- Subtitle of presentation
+ Title of document
+ Subtitle of document
<blank line>
- Presenter Name
+ Author Name
Job title, Company
joe@example.com
http://url/
@twitter_name
-The presenter section may contain a mixture of text, twitter names, and links.
-Only the plain text lines will be displayed on the presentation front page.
+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.
Multiple presenters may be specified, separated by a blank line.
@@ -44,10 +45,14 @@
Some Text
+ ** Subsection
+
- bullets
- more bullets
- a bullet with
+ *** Sub-subsection
+
Some More text
Preformatted text
diff --git a/present/parse.go b/present/parse.go
index 40dc276..86f8914 100644
--- a/present/parse.go
+++ b/present/parse.go
@@ -7,7 +7,6 @@
import (
"bytes"
"errors"
- "flag"
"fmt"
"html/template"
"io"
@@ -21,8 +20,7 @@
)
var (
- slideTemplate = flag.String("template", "", "alternate slide template file")
- parsers = make(map[string]func(string, int, string) (Elem, error))
+ parsers = make(map[string]func(string, int, string) (Elem, error))
funcs = template.FuncMap{
"style": style,
@@ -31,7 +29,7 @@
// Register binds the named action, which does not being with a period, to the
// specified parser and template function to be invoked when the name, with a
-// period, appears in the slide input text.
+// period, appears in the present input text.
// The function argument is an optional template function that is available
// inside templates under that name.
func Register(name string, parser func(fileName string, lineNumber int, inputLine string) (Elem, error), function interface{}) {
@@ -44,21 +42,37 @@
}
}
-// renderSlides reads the slide file, builds its template representation,
+// extensions maps the presentable file extensions to the name of the
+// template to be executed.
+var extensions = map[string]string{
+ ".slide": "templates/slides.tmpl",
+ ".article": "templates/article.tmpl",
+}
+
+func isDoc(path string) bool {
+ _, ok := extensions[filepath.Ext(path)]
+ return ok
+}
+
+// renderDoc reads the present file, builds its template representation,
// and executes the template, sending output to w.
-func renderSlides(w io.Writer, base, slideFile string) error {
- // Read the input and build the slide structure.
- pres, err := parse(slideFile, 0)
+func renderDoc(w io.Writer, base, docFile string) error {
+ // Read the input and build the doc structure.
+ pres, err := parse(docFile, 0)
if err != nil {
return err
}
- // Locate the template file.
- name := filepath.Join(base, "slide.tmpl")
- if *slideTemplate != "" {
- name = *slideTemplate
+ // Find which template should be executed.
+ ext := filepath.Ext(docFile)
+ tmplPath, ok := extensions[ext]
+ if !ok {
+ return fmt.Errorf("no template for extension %v", ext)
}
+ // Locate the template file.
+ name := filepath.Join(base, tmplPath)
+
// Read and parse the input.
tmpl := template.New(name).Funcs(funcs)
if _, err := tmpl.ParseFiles(name); err != nil {
@@ -68,27 +82,27 @@
pres.Template = tmpl
// Execute the template.
- return tmpl.ExecuteTemplate(w, "slides", pres)
+ return tmpl.ExecuteTemplate(w, "root", pres)
}
-// Pres represents an entire presentation.
-type Pres struct {
- Title string
- Subtitle string
- Presenters []Presenter
- Slide []Slide
- Template *template.Template
+// Doc represents an entire document.
+type Doc struct {
+ Title string
+ Subtitle string
+ Authors []Author
+ Sections []Section
+ Template *template.Template
}
-// Presenter represents the person who wrote and/or is giving the presentation.
-type Presenter struct {
+// 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 presenter details.
-// This is used to display the presenters' name, job title, and company
+// 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 *Presenter) TextElem() (elems []Elem) {
+func (p *Author) TextElem() (elems []Elem) {
for _, el := range p.Elem {
if _, ok := el.(Text); !ok {
break
@@ -98,14 +112,45 @@
return
}
-// Slide represents a single presentation slide.
-type Slide struct {
- Number int
+// 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
Elem []Elem
+ Doc *Doc
}
-// Elem defines the interface for a slide element.
+func (s Section) Sections() (sections []Section) {
+ for _, e := range s.Elem {
+ if s, ok := e.(Section); ok {
+ sections = append(sections, s)
+ }
+ }
+ 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) HTML(tmpl *template.Template) (template.HTML, error) {
+ return execTemplate(tmpl, "section", s)
+}
+
+// Elem defines the interface for a present element.
// That is, something that can render itself in HTML.
type Elem interface {
HTML(t *template.Template) (template.HTML, error)
@@ -196,32 +241,48 @@
titlesOnly parseMode = 1
)
-// parse parses the presentation in the file specified by name.
-func parse(name string, mode parseMode) (*Pres, error) {
- pres := new(Pres)
+// parse parses the document in the file specified by name.
+func parse(name string, mode parseMode) (*Doc, error) {
+ doc := new(Doc)
lines, err := readLines(name)
if err != nil {
return nil, err
}
var ok bool
// First non-empty line starts title.
- pres.Title, ok = lines.nextNonEmpty()
+ doc.Title, ok = lines.nextNonEmpty()
if !ok {
return nil, errors.New("no title")
}
- pres.Subtitle, ok = lines.next()
+ doc.Subtitle, ok = lines.next()
if !ok {
return nil, errors.New("no subtitle")
}
if mode&titlesOnly > 0 {
- return pres, nil
+ return doc, nil
}
- // Presenters
- pres.Presenters, err = parsePresenters(lines)
- // Slides
- for i := 0; ; i++ {
- var slide Slide
- slide.Number = i
+ // Authors
+ if doc.Authors, err = parseAuthors(lines); err != nil {
+ return nil, err
+ }
+ // Sections
+ if doc.Sections, err = parseSections(name, lines, []int{}, doc); err != nil {
+ return nil, err
+ }
+ return doc, nil
+}
+
+// lesserHeading returns true if text is a heading of a lesser or equal level
+// than that denoted by prefix.
+func lesserHeading(text, prefix string) bool {
+ return strings.HasPrefix(text, "*") && !strings.HasPrefix(text, prefix+"*")
+}
+
+// parseSections parses Sections from lines for the section level indicated by
+// number (a nil number indicates the top level).
+func parseSections(name string, lines *Lines, number []int, doc *Doc) ([]Section, error) {
+ var sections []Section
+ for i := 1; ; i++ {
// Next non-empty line is title.
text, ok := lines.nextNonEmpty()
for ok && text == "" {
@@ -230,12 +291,18 @@
if !ok {
break
}
- if !strings.HasPrefix(text, "* ") {
- return nil, fmt.Errorf("%s:%d bad title %q", name, lines.line, text)
+ prefix := strings.Repeat("*", len(number)+1)
+ if !strings.HasPrefix(text, prefix+" ") {
+ lines.back()
+ break
}
- slide.Title = text[2:]
+ section := Section{
+ Number: append(append([]int{}, number...), i),
+ Title: text[len(prefix)+1:],
+ Doc: doc,
+ }
text, ok = lines.nextNonEmpty()
- for ok && !strings.HasPrefix(text, "* ") {
+ for ok && !lesserHeading(text, prefix) {
var e Elem
r, _ := utf8.DecodeRuneInString(text)
switch {
@@ -267,16 +334,26 @@
}
lines.back()
e = List{Bullet: b}
+ case strings.HasPrefix(text, prefix+"* "):
+ lines.back()
+ subsecs, err := parseSections(name, lines, section.Number, doc)
+ if err != nil {
+ return nil, err
+ }
+ for _, ss := range subsecs {
+ section.Elem = append(section.Elem, ss)
+ }
case strings.HasPrefix(text, "."):
args := strings.Fields(text)
parser := parsers[args[0]]
if parser == nil {
return nil, fmt.Errorf("%s:%d: unknown command %q\n", name, lines.line, text)
}
- e, err = parser(name, lines.line, text)
+ t, err := parser(name, lines.line, text)
if err != nil {
return nil, err
}
+ e = t
default:
var l []string
for ok && strings.TrimSpace(text) != "" {
@@ -294,20 +371,20 @@
}
}
if e != nil {
- slide.Elem = append(slide.Elem, e)
+ section.Elem = append(section.Elem, e)
}
text, ok = lines.nextNonEmpty()
}
- if strings.HasPrefix(text, "* ") {
+ if strings.HasPrefix(text, "*") {
lines.back()
}
- pres.Slide = append(pres.Slide, slide)
+ sections = append(sections, section)
}
- return pres, nil
+ return sections, nil
}
-func parsePresenters(lines *Lines) (pres []Presenter, err error) {
- // This grammar demarcates presenters with blanks.
+func parseAuthors(lines *Lines) (authors []Author, err error) {
+ // This grammar demarcates authors with blanks.
// Skip blank lines.
if _, ok := lines.nextNonEmpty(); !ok {
@@ -315,27 +392,27 @@
}
lines.back()
- var p *Presenter
+ var a *Author
for {
text, ok := lines.next()
if !ok {
return nil, errors.New("unexpected EOF")
}
- // If we find a slide heading, we're done.
+ // If we find a section heading, we're done.
if strings.HasPrefix(text, "* ") {
lines.back()
break
}
- // If we encounter a blank we're done with this presenter.
- if p != nil && len(text) == 0 {
- pres = append(pres, *p)
- p = nil
+ // 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 p == nil {
- p = new(Presenter)
+ if a == nil {
+ a = new(Author)
}
// Parse the line. Those that
@@ -359,12 +436,12 @@
if el == nil {
el = Text{Lines: []string{text}}
}
- p.Elem = append(p.Elem, el)
+ a.Elem = append(a.Elem, el)
}
- if p != nil {
- pres = append(pres, *p)
+ if a != nil {
+ authors = append(authors, *a)
}
- return pres, nil
+ return authors, nil
}
func parseURL(text string) Elem {
diff --git a/present/static/article.css b/present/static/article.css
new file mode 100644
index 0000000..cb7b381
--- /dev/null
+++ b/present/static/article.css
@@ -0,0 +1,157 @@
+body {
+ margin: 0;
+ font-family: Helvetica, Arial, sans-serif;
+ font-size: 16px;
+}
+pre,
+code {
+ font-family: Menlo, monospace;
+ font-size: 14px;
+}
+pre {
+ line-height: 18px;
+}
+a {
+ color: #375EAB;
+ text-decoration: none;
+}
+a:hover {
+ text-decoration: underline;
+}
+p, pre, ul, ol {
+ margin: 20px;
+}
+pre {
+ background: #e9e9e9;
+ padding: 10px;
+
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+
+h1, h2, h3, h4 {
+ margin: 20px 0;
+ padding: 0;
+ color: #375EAB;
+ font-weight: bold;
+}
+h1 {
+ font-size: 24px;
+}
+h2 {
+ font-size: 20px;
+ background: #E0EBF5;
+ padding: 2px 5px;
+}
+h3 {
+ font-size: 20px;
+}
+h3, h4 {
+ margin: 20px 5px;
+}
+h4 {
+ font-size: 16px;
+}
+
+div#heading {
+ float: left;
+ margin: 0 0 10px 0;
+ padding: 21px 0;
+ font-size: 20px;
+ font-weight: normal;
+}
+
+div#topbar {
+ background: #E0EBF5;
+ height: 64px;
+ overflow: hidden;
+}
+
+body {
+ text-align: center;
+}
+div#page {
+ width: 100%;
+}
+div#page > .container,
+div#topbar > .container {
+ text-align: left;
+ margin-left: auto;
+ margin-right: auto;
+ padding: 0 20px;
+ width: 900px;
+}
+div#page.wide > .container,
+div#topbar.wide > .container {
+ width: auto;
+}
+
+div#footer {
+ text-align: center;
+ color: #666;
+ font-size: 14px;
+ margin: 40px 0;
+}
+
+/* always show topbar for large screens */
+@media screen and (min-width: 130ex) and (min-height: 300px) {
+ /* 130ex -> wide enough so that title isn't below buttons */
+
+ div#topbar.wide {
+ position: fixed;
+ z-index: 1;
+ top: 0;
+ width: 100%;
+ height: 63px;
+ border-bottom: 1px solid #B0BBC5;
+ }
+
+ div#page.wide {
+ position: fixed;
+ top: 64px; /* to match topbar */
+ bottom: 0px;
+ overflow: auto;
+ margin-left: auto;
+ margin-right: auto;
+ }
+}
+
+.author p {
+ margin: 20, 0, 0, 0px;
+}
+
+div.output pre {
+ background: black;
+}
+div.output .stdout {
+ color: #e6e6e6;
+}
+div.output .stderr {
+ color: rgb(244, 74, 63);
+}
+div.output .system {
+ color: rgb(255, 209, 77)
+}
+
+#toc {
+ float: right;
+ margin: 0px 10px;
+ padding: 10px;
+ border: 1px solid #e5ecf9;
+ border-radius: 1em;
+ -moz-border-radius: 1em;
+ background-color: white;
+ max-width: 33%;
+}
+
+#toc ul, #toc a {
+ list-style-type: none;
+ padding-left: 10px;
+ color: black;
+ margin: 0px;
+}
+
+.buttons {
+ margin-left: 1.0em;
+}
\ No newline at end of file
diff --git a/present/static/playground.js b/present/static/playground.js
index de3bd95..79aeb07 100644
--- a/present/static/playground.js
+++ b/present/static/playground.js
@@ -76,6 +76,7 @@
function onRun() {
outpre.innerHTML = "";
output.style.display = "block";
+ run.style.display = "none";
sendMessage({Id: id, Kind: "run", Body: text(code)});
}
@@ -86,10 +87,10 @@
function onClose() {
onKill();
output.style.display = "none";
+ run.style.display = "inline-block";
}
var run = document.createElement('button');
- run.contenteditable = false;
run.innerHTML = 'Run';
run.addEventListener("click", onRun, false);
var run2 = document.createElement('button');
@@ -102,6 +103,12 @@
close.innerHTML = 'Close';
close.addEventListener("click", onClose, false);
+ var button = document.createElement('div');
+ button.classList.add('buttons');
+ button.appendChild(run);
+ // Hack to simulate insertAfter
+ code.parentNode.insertBefore(button, code.nextSibling)
+
var buttons = document.createElement('div');
buttons.classList.add('buttons');
buttons.appendChild(run2);
@@ -112,9 +119,7 @@
output.appendChild(buttons);
output.appendChild(outpre);
output.style.display = "none";
-
- code.appendChild(run);
- code.parentNode.appendChild(output);
+ code.parentNode.insertBefore(output, button.nextSibling)
outputs[id] = outpre;
}
diff --git a/present/static/styles.css b/present/static/styles.css
index 08d9193..202a483 100755
--- a/present/static/styles.css
+++ b/present/static/styles.css
@@ -399,9 +399,16 @@
div.output .system {
color: rgb(255, 209, 77)
}
-div.output .buttons,
-div.playground button {
+.buttons {
+ position: relative;
+ float: right;
+ top: -60px;
+ right: 10px;
+}
+div.output .buttons {
position: absolute;
+ float: none;
+ top: auto;
right: 5px;
bottom: 5px;
}
diff --git a/present/templates/article.tmpl b/present/templates/article.tmpl
new file mode 100644
index 0000000..8153cd9
--- /dev/null
+++ b/present/templates/article.tmpl
@@ -0,0 +1,93 @@
+{/*
+
+This is the article template. It defines how articles are formatted.
+
+The "root" template is the base HTML document. The "list", "text", "code",
+"image", and "link" templates are used by the various formatting helpers.
+
+*/}
+{{define "root"}}
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>{{.Title}}</title>
+ <link type="text/css" rel="stylesheet" href="/static/article.css">
+ <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
+ <meta charset='utf-8'>
+ </head>
+
+ <body>
+ <div id="topbar" class="wide">
+ <div class="container">
+ <div id="heading">{{.Title}}
+ {{with .Subtitle}}{{.}}{{end}}
+ </div>
+ </div>
+ </div>
+ <div id="page" class="wide">
+ <div class="container">
+ {{with $toc := .Sections}}
+ <div id="toc">
+ {{range .}}{{template "TOC" .}}{{end}}
+ </div>
+ {{end}}
+
+ {{range .Sections}}
+ {{.HTML $.Template}}
+ {{end}}{{/* of Section block */}}
+
+ <h2>Authors</h2>
+ {{range .Authors}}
+ <div class="author">
+ {{range .Elem}}{{.HTML $.Template}}{{end}}
+ </div>
+ {{end}}
+ </div>
+ </div>
+ <script src='/static/playground.js'></script>
+ </body>
+</html>
+{{end}}
+
+{{define "TOC"}}
+ <ul>
+ <li><a href="#TOC_{{.FormattedNumber}}">{{.Title}}</a></li>
+ {{with .Sections}}
+ <ul>
+ {{range .}}{{template "TOC" .}}{{end}}
+ </ul>
+ {{end}}
+ </ul>
+{{end}}
+
+{{define "section"}}
+ <h{{len .Number}} id="TOC_{{.FormattedNumber}}">{{.FormattedNumber}} {{.Title}}</h{{len .Number}}>
+ {{range .Elem}}{{.HTML $.Doc.Template}}{{end}}
+{{end}}
+
+{{define "list"}}
+ <ul>
+ {{range .Bullet}}
+ <li>{{style .}}</li>
+ {{end}}
+ </ul>
+{{end}}
+
+{{define "text"}}
+ {{if .Pre}}
+ <div class="code"><pre>{{range .Lines}}{{.}}{{end}}</pre></div>
+ {{else}}
+ <p>
+ {{range $i, $l := .Lines}}{{if $i}}<br>
+ {{end}}{{style $l}}{{end}}
+ </p>
+ {{end}}
+{{end}}
+
+{{define "code"}}
+ <div class="code{{if .Play}} playground{{end}}" contenteditable="true">{{code .}}</div>
+{{end}}
+
+{{define "image"}}<div class="image">{{image .File .Args}}</div>{{end}}
+
+{{define "link"}}<p class="link">{{link .URL .Args}}</p>{{end}}
diff --git a/present/slide.tmpl b/present/templates/slides.tmpl
old mode 100644
new mode 100755
similarity index 81%
rename from present/slide.tmpl
rename to present/templates/slides.tmpl
index 2b720da..1774f58
--- a/present/slide.tmpl
+++ b/present/templates/slides.tmpl
@@ -2,11 +2,11 @@
This is the slide template. It defines how presentations are formatted.
-The "slide" template is the base HTML document. The "list", "text", "code",
+The "root" template is the base HTML document. The "list", "text", "code",
"image", and "link" templates are used by the various formatting helpers.
*/}
-{{define "slides"}}
+{{define "root"}}
<!DOCTYPE html>
<html>
<head>
@@ -22,14 +22,14 @@
<article>
<h1>{{.Title}}</h1>
{{with .Subtitle}}<h3>{{.}}</h3>{{end}}
- {{range .Presenters}}
+ {{range .Authors}}
<div class="presenter">
{{range .TextElem}}{{.HTML $.Template}}{{end}}
</div>
{{end}}
</article>
- {{range $i, $s := .Slide}}
+ {{range $i, $s := .Sections}}
<!-- start of slide {{$s.Number}} -->
<article>
{{if $s.Elem}}
@@ -43,8 +43,8 @@
{{end}}{{/* of Slide block */}}
<article>
- <h3>Thank you</h3>
- {{range .Presenters}}
+ <h3>Thank you</h1>
+ {{range .Authors}}
<div class="presenter">
{{range .Elem}}{{.HTML $.Template}}{{end}}
</div>
@@ -56,6 +56,11 @@
</html>
{{end}}
+{{define "section"}}
+ <h{{len .Number}} id="TOC_{{.FormattedNumber}}">{{.FormattedNumber}} {{.Title}}</h{{len .Number}}>
+ {{range .Elem}}{{.HTML $.Doc.Template}}{{end}}
+{{end}}
+
{{define "list"}}
<ul>
{{range .Bullet}}
@@ -81,4 +86,4 @@
{{define "image"}}<div class="image">{{image .File .Args}}</div>{{end}}
-{{define "link"}}<p class="link">{{link .URL .Args}}</p>{{end}}
+{{define "link"}}<p class="link">{{link .URL .Args}}</p>{{end}}
\ No newline at end of file