go.talks/present: support fontifying markers

R=adg
CC=golang-dev
https://golang.org/cl/6625073
diff --git a/present/code.go b/present/code.go
index 448a593..c322f96 100644
--- a/present/code.go
+++ b/present/code.go
@@ -6,7 +6,6 @@
 
 import (
 	"fmt"
-	"html"
 	"html/template"
 	"io/ioutil"
 	"log"
@@ -292,15 +291,3 @@
 func hide(text string) string {
 	return fmt.Sprintf(`<pre style="display: none">%s</pre>`, template.HTMLEscapeString(text))
 }
-
-const codifyChar = "`"
-
-var codifyRE = regexp.MustCompile(fmt.Sprintf("%s[^%s]+%s", codifyChar, codifyChar, codifyChar))
-
-func codify(s string) template.HTML {
-	s = html.EscapeString(s)
-	repl := func(s string) string {
-		return "<code>" + s[1:len(s)-1] + "</code>"
-	}
-	return template.HTML(codifyRE.ReplaceAllStringFunc(s, repl))
-}
diff --git a/present/doc.go b/present/doc.go
index 5b481d8..f3d9aca 100644
--- a/present/doc.go
+++ b/present/doc.go
@@ -40,7 +40,6 @@
 	* Title of slide or section (must have asterisk)
 
 	Some Text
-	(Strings enclosed by `backquotes` will be presented as source code.)
 
 	- bullets
 	- more bullets
@@ -66,8 +65,22 @@
 
 Lines starting with # in column 1 are commentary.
 
-Within the input for plain text or lists, text included in back
-quotes `xxx` will be presented in program font.
+Fonts:
+
+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).
+Unmatched markers appear as plain text.
+Within marked text, a single marker character becomes a space
+and a doubled single marker quotes the marker character.
+
+	_italic_
+	*bold*
+	`program`
+	_this_is_all_italic_
+	_Why_use_scoped__ptr_? Use plain ***ptr* instead.
+
+Functions:
 
 A number of template functions are available through invocations
 in the input text. Each such invocation contains a period as the
diff --git a/present/parse.go b/present/parse.go
index c294c10..26d7d0e 100644
--- a/present/parse.go
+++ b/present/parse.go
@@ -22,7 +22,7 @@
 	parsers       = make(map[string]func(string, int, string) (Elem, error))
 
 	funcs = template.FuncMap{
-		"codify": codify,
+		"style": style,
 	}
 )
 
diff --git a/present/slide.tmpl b/present/slide.tmpl
index d4caba7..e3ecdaa 100755
--- a/present/slide.tmpl
+++ b/present/slide.tmpl
@@ -64,7 +64,7 @@
 {{define "list"}}
   <ul>
   {{range .Bullet}}
-    <li>{{codify .}}</li>
+    <li>{{style .}}</li>
   {{end}}
   </ul>
 {{end}}
@@ -75,7 +75,7 @@
   {{else}}
   <p>
     {{range $i, $l := .Lines}}{{if $i}}<br>
-    {{end}}{{codify $l}}{{end}}
+    {{end}}{{style $l}}{{end}}
   </p>
   {{end}}
 {{end}}
diff --git a/present/style.go b/present/style.go
new file mode 100644
index 0000000..3943af7
--- /dev/null
+++ b/present/style.go
@@ -0,0 +1,117 @@
+// 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 main
+
+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 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.IndexAny(s, "`_*") == -1 {
+		return s
+	}
+	words := split(s)
+	var b bytes.Buffer
+Word:
+	for w, word := range words {
+		if len(word) < 2 {
+			continue Word
+		}
+		char := word[0] // ASCII is OK.
+		open := ""
+		close := ""
+		switch char {
+		default:
+			continue Word
+		case '_':
+			open = "<i>"
+			close = "</i>"
+		case '*':
+			open = "<b>"
+			close = "</b>"
+		case '`':
+			open = "<code>"
+			close = "</code>"
+		}
+		// Terminal punctuation is OK but must be peeled off.
+		last := strings.LastIndex(word, word[:1])
+		if last == 0 {
+			continue Word
+		}
+		head, tail := word[:last+1], word[last+1:]
+		for _, r := range tail {
+			if !strings.ContainsRune(`.,;:()!?—–'"`, r) {
+				continue Word
+			}
+		}
+		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.
+func split(s string) []string {
+	words := make([]string, 0, 10)
+	prevWasSpace := false
+	mark := 0
+	for i, r := range s {
+		isSpace := unicode.IsSpace(r)
+		if isSpace != prevWasSpace && i > mark {
+			words = append(words, s[mark:i])
+			mark = i
+		}
+		prevWasSpace = isSpace
+	}
+	if mark < len(s) {
+		words = append(words, s[mark:])
+	}
+	return words
+}
diff --git a/present/style_test.go b/present/style_test.go
new file mode 100644
index 0000000..3ea804b
--- /dev/null
+++ b/present/style_test.go
@@ -0,0 +1,80 @@
+// 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 main
+
+import (
+	"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", " "}},
+	}
+	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>."},
+		{"_Why_use_scoped__ptr_? Use plain ***ptr* instead.", "<i>Why use scoped_ptr</i>? Use plain <b>*ptr</b> instead."},
+	}
+	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>."},
+	}
+	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)
+		}
+	}
+}