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