go.talks/pkg/present: Adding inline links with style.

Read doc.go for more details.

R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/6847128
diff --git a/pkg/present/doc.go b/pkg/present/doc.go
index d1a8f27..d86a85c 100644
--- a/pkg/present/doc.go
+++ b/pkg/present/doc.go
@@ -78,6 +78,11 @@
 	_this_is_all_italic_
 	_Why_use_scoped__ptr_? Use plain ***ptr* instead.
 
+Inline links:
+
+Links can be included in any text with the form [[url][label]], or
+[[url]] to use the URL itself as the label.
+
 Functions:
 
 A number of template functions are available through invocations
diff --git a/pkg/present/link.go b/pkg/present/link.go
index 798a64c..cab1a03 100644
--- a/pkg/present/link.go
+++ b/pkg/present/link.go
@@ -46,5 +46,40 @@
 	default:
 		label = strings.Join(arg, " ")
 	}
-	return template.HTML(fmt.Sprintf(`<a href=%q>%s</a>`, url.String(), label)), nil
+	return template.HTML(renderLink(url.String(), label)), nil
+}
+
+func renderLink(url, text string) string {
+	text = font(text)
+	if text == "" {
+		text = url
+	}
+	return fmt.Sprintf(`<a href="%s" target="_blank">%s</a>`, url, 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 len(s) < 2 || s[:2] != "[[" {
+		return
+	}
+	end := strings.Index(s, "]]")
+	if end == -1 {
+		return
+	}
+	urlEnd := strings.Index(s, "]")
+	url := s[2:urlEnd]
+	const badURLChars = `<>"{}|\^~[] ` + "`" // per RFC1738 section 2.2
+	if strings.ContainsAny(url, badURLChars) {
+		return
+	}
+	if urlEnd == end {
+		return renderLink(url, ""), end + 2
+	}
+	if s[urlEnd:urlEnd+2] != "][" {
+		return
+	}
+	text := s[urlEnd+2 : end]
+	return renderLink(url, text), end + 2
 }
diff --git a/pkg/present/link_test.go b/pkg/present/link_test.go
new file mode 100644
index 0000000..7c90e18
--- /dev/null
+++ b/pkg/present/link_test.go
@@ -0,0 +1,38 @@
+// 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", "http://golang.org", 21},
+		{"[[http://golang.org][]]", "http://golang.org", "http://golang.org", 23},
+		{"[[http://golang.org]] this is ignored", "http://golang.org", "http://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},
+	}
+
+	for _, 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("parseInlineLink(%q):\ngot\t%q, %d\nwant\t%q, %d", test.in, link, length, a, test.length)
+		}
+	}
+}
diff --git a/pkg/present/style.go b/pkg/present/style.go
index 169464a..4a3f99f 100644
--- a/pkg/present/style.go
+++ b/pkg/present/style.go
@@ -36,7 +36,7 @@
 
 // font returns s with font indicators turned into HTML font tags.
 func font(s string) string {
-	if strings.IndexAny(s, "`_*") == -1 {
+	if strings.IndexAny(s, "[`_*") == -1 {
 		return s
 	}
 	words := split(s)
@@ -46,6 +46,10 @@
 		if len(word) < 2 {
 			continue Word
 		}
+		if link, _ := parseInlineLink(word); link != "" {
+			words[w] = link
+			continue Word
+		}
 		const punctuation = `.,;:()!?—–'"`
 		const marker = "_*`"
 		// Initial punctuation is OK but must be peeled off.
@@ -121,9 +125,15 @@
 	mark := 0
 	for i, r := range s {
 		isSpace := unicode.IsSpace(r)
-		if isSpace != prevWasSpace && i > mark {
-			words = append(words, s[mark:i])
-			mark = i
+		if i > mark {
+			if isSpace != prevWasSpace {
+				words = append(words, s[mark:i])
+				mark = i
+			}
+			if _, length := parseInlineLink(s[i:]); length > 0 {
+				words = append(words, s[i:i+length])
+				mark = i + length
+			}
 		}
 		prevWasSpace = isSpace
 	}
diff --git a/pkg/present/style_test.go b/pkg/present/style_test.go
index a7088c3..049211a 100644
--- a/pkg/present/style_test.go
+++ b/pkg/present/style_test.go
@@ -19,6 +19,14 @@
 		{"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"}},
 	}
 	for _, test := range tests {
 		out := split(test.in)
@@ -49,6 +57,12 @@
 		{"(_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">http://golang.org</a> now`},
 	}
 	for _, test := range tests {
 		out := font(test.in)