diff --git a/cmd/present/static/styles.css b/cmd/present/static/styles.css
index 2d8d3544d1e..91b8bca80c8 100644
--- a/cmd/present/static/styles.css
+++ b/cmd/present/static/styles.css
@@ -532,3 +532,12 @@ figcaption {
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
}
+
+blockquote {
+ padding: 5px 20px;
+ border-left: 5px solid #999;
+}
+blockquote cite {
+ font-size: 85%;
+ color: #999;
+}
diff --git a/cmd/present/templates/action.tmpl b/cmd/present/templates/action.tmpl
index 8bde042c90b..ad752cc9c3a 100644
--- a/cmd/present/templates/action.tmpl
+++ b/cmd/present/templates/action.tmpl
@@ -60,3 +60,10 @@ It determines how the formatting actions are rendered.
{{define "html"}}{{.HTML}}{{end}}
{{define "caption"}}{{style .Text}}{{end}}
+
+{{define "quote"}}
+
+ {{style .Text}}
+ {{with .Citation}}— {{style .}}{{end}}
+
+{{end}}
diff --git a/present/doc.go b/present/doc.go
index 45039b646d2..cf3fcfc3c16 100644
--- a/present/doc.go
+++ b/present/doc.go
@@ -63,6 +63,7 @@ After that come slides/sections, each after a blank line:
.link http://foo label
.html file.html
.caption _Gopher_ by [[https://www.instagram.com/reneefrench/][Renée French]]
+ .quote Never memorize something that you can look up //CITATION: Albert Einstein
Again, more text
@@ -214,6 +215,16 @@ processing styling and links as in standard text lines.
.caption _Gopher_ by [[http://www.reneefrench.com][Renée French]]
+quote:
+
+The template uses the function "quote" to inject quotes and citations.
+
+The text after ".quote" is embedded in a blockquote element after
+processing styling. The text after the optional "//CITATION:" is treated
+as a citation and embedded in a cite element after processing styling.
+
+ .quote Never memorize something that you can look up //CITATION: Albert Einstein
+
iframe:
The function "iframe" injects iframes (pages inside pages).
diff --git a/present/quote.go b/present/quote.go
new file mode 100644
index 00000000000..d47b6a7ab92
--- /dev/null
+++ b/present/quote.go
@@ -0,0 +1,50 @@
+// Copyright 2018 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 (
+ "fmt"
+ "strings"
+)
+
+func init() {
+ Register("quote", parseQuote)
+}
+
+type Quote struct {
+ Text string
+ Citation string
+}
+
+func (c Quote) TemplateName() string { return "quote" }
+
+const citationToken = "//CITATION:"
+
+// parseQuote parses a quote present directive. Its syntax:
+// .quote [citation]
+func parseQuote(_ *Context, sourceFile string, sourceLine int, cmd string) (Elem, error) {
+
+ cmd = strings.TrimSpace(strings.TrimPrefix(cmd, ".quote"))
+
+ tokens := strings.Split(cmd, citationToken)
+
+ text := strings.TrimSpace(strings.TrimPrefix(tokens[0], ".quote"))
+ citation := ""
+
+ if text == "" {
+ return nil, fmt.Errorf("%s:%d invalid quote syntax", sourceFile, sourceLine)
+ }
+
+ if len(tokens) == 1 {
+ return Quote{text, citation}, nil
+ }
+
+ citation = strings.TrimSpace(tokens[1])
+ if citation == "" || len(tokens) > 2 {
+ return nil, fmt.Errorf("%s:%d invalid citation syntax", sourceFile, sourceLine)
+ }
+
+ return Quote{text, citation}, nil
+}
diff --git a/present/quote_test.go b/present/quote_test.go
new file mode 100644
index 00000000000..f260a81c049
--- /dev/null
+++ b/present/quote_test.go
@@ -0,0 +1,78 @@
+// Copyright 2018 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 (
+ "strings"
+ "testing"
+)
+
+func TestQuoteParsing(t *testing.T) {
+ var tests = []struct {
+ name string
+ cmd string
+ err string
+ Quote
+ }{
+ {
+ name: "quote with invalid citation syntax",
+ cmd: ".quote a quote //CITATION: ",
+ err: "invalid citation syntax",
+ },
+ {
+ name: "quote without text and valid citation",
+ cmd: ".quote //CITATION: citation",
+ err: "invalid quote syntax",
+ },
+ {
+ name: "quote with text",
+ cmd: ".quote some text",
+ Quote: Quote{
+ Text: "some text",
+ Citation: "",
+ },
+ },
+ {
+ name: "quote with text and citation",
+ cmd: ".quote some text //CITATION: other text",
+ Quote: Quote{
+ Text: "some text",
+ Citation: "other text",
+ },
+ },
+ }
+
+ for _, test := range tests {
+ element, err := parseQuote(nil, "test.slide", 0, test.cmd)
+
+ if err != nil {
+ if test.err == "" {
+ t.Errorf("%s: unexpected error %v", test.name, err)
+ } else if !strings.Contains(err.Error(), test.err) {
+ t.Errorf("%s: expected error %s; got %v", test.name, test.err, err)
+ }
+ continue
+ }
+
+ if test.err != "" {
+ t.Errorf("%s: expected error %s; but got none", test.name, test.err)
+ continue
+ }
+
+ quote, ok := element.(Quote)
+ if !ok {
+ t.Errorf("%s: expected a Code value; got %T", test.name, quote)
+ continue
+ }
+
+ if quote.Text != test.Text {
+ t.Errorf("%s: expected Text %s; got %s", test.name, test.Text, quote.Text)
+ }
+
+ if quote.Citation != test.Citation {
+ t.Errorf("%s: expected Citation %s; got %s", test.name, test.Citation, quote.Citation)
+ }
+ }
+}