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)