Bar`, "FooBar"},
+ {`I <3 Ponies!`, `I <3 Ponies!`},
+ {``, ``},
+ }
+
+ for _, test := range tests {
+ if got := stripTags(test.input); got != test.want {
+ t.Errorf("%q: want %q, got %q", test.input, test.want, got)
+ }
+ }
+}
+
+func BenchmarkHTMLNospaceEscaper(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ htmlNospaceEscaper("The
quick,\r\n
brown fox jumps\u2028over the
dog")
+ }
+}
+
+func BenchmarkHTMLNospaceEscaperNoSpecials(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ htmlNospaceEscaper("The_quick,_brown_fox_jumps_over_the_lazy_dog.")
+ }
+}
+
+func BenchmarkStripTags(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ stripTags("The
quick,\r\n
brown fox jumps\u2028over the
dog")
+ }
+}
+
+func BenchmarkStripTagsNoSpecials(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ stripTags("The quick, brown fox jumps over the lazy dog.")
+ }
+}
diff --git a/template/js.go b/template/js.go
new file mode 100644
index 00000000..999a61ed
--- /dev/null
+++ b/template/js.go
@@ -0,0 +1,362 @@
+// Copyright 2011 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 template
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "strings"
+ "unicode/utf8"
+)
+
+// nextJSCtx returns the context that determines whether a slash after the
+// given run of tokens starts a regular expression instead of a division
+// operator: / or /=.
+//
+// This assumes that the token run does not include any string tokens, comment
+// tokens, regular expression literal tokens, or division operators.
+//
+// This fails on some valid but nonsensical JavaScript programs like
+// "x = ++/foo/i" which is quite different than "x++/foo/i", but is not known to
+// fail on any known useful programs. It is based on the draft
+// JavaScript 2.0 lexical grammar and requires one token of lookbehind:
+// http://www.mozilla.org/js/language/js20-2000-07/rationale/syntax.html
+func nextJSCtx(s []byte, preceding jsCtx) jsCtx {
+ s = bytes.TrimRight(s, "\t\n\f\r \u2028\u2029")
+ if len(s) == 0 {
+ return preceding
+ }
+
+ // All cases below are in the single-byte UTF-8 group.
+ switch c, n := s[len(s)-1], len(s); c {
+ case '+', '-':
+ // ++ and -- are not regexp preceders, but + and - are whether
+ // they are used as infix or prefix operators.
+ start := n - 1
+ // Count the number of adjacent dashes or pluses.
+ for start > 0 && s[start-1] == c {
+ start--
+ }
+ if (n-start)&1 == 1 {
+ // Reached for trailing minus signs since "---" is the
+ // same as "-- -".
+ return jsCtxRegexp
+ }
+ return jsCtxDivOp
+ case '.':
+ // Handle "42."
+ if n != 1 && '0' <= s[n-2] && s[n-2] <= '9' {
+ return jsCtxDivOp
+ }
+ return jsCtxRegexp
+ // Suffixes for all punctuators from section 7.7 of the language spec
+ // that only end binary operators not handled above.
+ case ',', '<', '>', '=', '*', '%', '&', '|', '^', '?':
+ return jsCtxRegexp
+ // Suffixes for all punctuators from section 7.7 of the language spec
+ // that are prefix operators not handled above.
+ case '!', '~':
+ return jsCtxRegexp
+ // Matches all the punctuators from section 7.7 of the language spec
+ // that are open brackets not handled above.
+ case '(', '[':
+ return jsCtxRegexp
+ // Matches all the punctuators from section 7.7 of the language spec
+ // that precede expression starts.
+ case ':', ';', '{':
+ return jsCtxRegexp
+ // CAVEAT: the close punctuators ('}', ']', ')') precede div ops and
+ // are handled in the default except for '}' which can precede a
+ // division op as in
+ // ({ valueOf: function () { return 42 } } / 2
+ // which is valid, but, in practice, developers don't divide object
+ // literals, so our heuristic works well for code like
+ // function () { ... } /foo/.test(x) && sideEffect();
+ // The ')' punctuator can precede a regular expression as in
+ // if (b) /foo/.test(x) && ...
+ // but this is much less likely than
+ // (a + b) / c
+ case '}':
+ return jsCtxRegexp
+ default:
+ // Look for an IdentifierName and see if it is a keyword that
+ // can precede a regular expression.
+ j := n
+ for j > 0 && isJSIdentPart(rune(s[j-1])) {
+ j--
+ }
+ if regexpPrecederKeywords[string(s[j:])] {
+ return jsCtxRegexp
+ }
+ }
+ // Otherwise is a punctuator not listed above, or
+ // a string which precedes a div op, or an identifier
+ // which precedes a div op.
+ return jsCtxDivOp
+}
+
+// regexpPrecederKeywords is a set of reserved JS keywords that can precede a
+// regular expression in JS source.
+var regexpPrecederKeywords = map[string]bool{
+ "break": true,
+ "case": true,
+ "continue": true,
+ "delete": true,
+ "do": true,
+ "else": true,
+ "finally": true,
+ "in": true,
+ "instanceof": true,
+ "return": true,
+ "throw": true,
+ "try": true,
+ "typeof": true,
+ "void": true,
+}
+
+var jsonMarshalType = reflect.TypeOf((*json.Marshaler)(nil)).Elem()
+
+// indirectToJSONMarshaler returns the value, after dereferencing as many times
+// as necessary to reach the base type (or nil) or an implementation of json.Marshal.
+func indirectToJSONMarshaler(a interface{}) interface{} {
+ v := reflect.ValueOf(a)
+ for !v.Type().Implements(jsonMarshalType) && v.Kind() == reflect.Ptr && !v.IsNil() {
+ v = v.Elem()
+ }
+ return v.Interface()
+}
+
+// jsValEscaper escapes its inputs to a JS Expression (section 11.14) that has
+// neither side-effects nor free variables outside (NaN, Infinity).
+func jsValEscaper(args ...interface{}) string {
+ var a interface{}
+ if len(args) == 1 {
+ a = indirectToJSONMarshaler(args[0])
+ switch t := a.(type) {
+ case JS:
+ return string(t)
+ case JSStr:
+ // TODO: normalize quotes.
+ return `"` + string(t) + `"`
+ case json.Marshaler:
+ // Do not treat as a Stringer.
+ case fmt.Stringer:
+ a = t.String()
+ }
+ } else {
+ for i, arg := range args {
+ args[i] = indirectToJSONMarshaler(arg)
+ }
+ a = fmt.Sprint(args...)
+ }
+ // TODO: detect cycles before calling Marshal which loops infinitely on
+ // cyclic data. This may be an unacceptable DoS risk.
+
+ b, err := json.Marshal(a)
+ if err != nil {
+ // Put a space before comment so that if it is flush against
+ // a division operator it is not turned into a line comment:
+ // x/{{y}}
+ // turning into
+ // x//* error marshalling y:
+ // second line of error message */null
+ return fmt.Sprintf(" /* %s */null ", strings.Replace(err.Error(), "*/", "* /", -1))
+ }
+
+ // TODO: maybe post-process output to prevent it from containing
+ // "", "", or " element,
+// or in an HTML5 event handler attribute such as onclick.
+func jsStrEscaper(args ...interface{}) string {
+ s, t := stringify(args...)
+ if t == contentTypeJSStr {
+ return replace(s, jsStrNormReplacementTable)
+ }
+ return replace(s, jsStrReplacementTable)
+}
+
+// jsRegexpEscaper behaves like jsStrEscaper but escapes regular expression
+// specials so the result is treated literally when included in a regular
+// expression literal. /foo{{.X}}bar/ matches the string "foo" followed by
+// the literal text of {{.X}} followed by the string "bar".
+func jsRegexpEscaper(args ...interface{}) string {
+ s, _ := stringify(args...)
+ s = replace(s, jsRegexpReplacementTable)
+ if s == "" {
+ // /{{.X}}/ should not produce a line comment when .X == "".
+ return "(?:)"
+ }
+ return s
+}
+
+// replace replaces each rune r of s with replacementTable[r], provided that
+// r < len(replacementTable). If replacementTable[r] is the empty string then
+// no replacement is made.
+// It also replaces runes U+2028 and U+2029 with the raw strings `\u2028` and
+// `\u2029`.
+func replace(s string, replacementTable []string) string {
+ var b bytes.Buffer
+ written := 0
+ for i, r := range s {
+ var repl string
+ switch {
+ case int(r) < len(replacementTable) && replacementTable[r] != "":
+ repl = replacementTable[r]
+ case r == '\u2028':
+ repl = `\u2028`
+ case r == '\u2029':
+ repl = `\u2029`
+ default:
+ continue
+ }
+ b.WriteString(s[written:i])
+ b.WriteString(repl)
+ written = i + utf8.RuneLen(r)
+ }
+ if written == 0 {
+ return s
+ }
+ b.WriteString(s[written:])
+ return b.String()
+}
+
+var jsStrReplacementTable = []string{
+ 0: `\0`,
+ '\t': `\t`,
+ '\n': `\n`,
+ '\v': `\x0b`, // "\v" == "v" on IE 6.
+ '\f': `\f`,
+ '\r': `\r`,
+ // Encode HTML specials as hex so the output can be embedded
+ // in HTML attributes without further encoding.
+ '"': `\x22`,
+ '&': `\x26`,
+ '\'': `\x27`,
+ '+': `\x2b`,
+ '/': `\/`,
+ '<': `\x3c`,
+ '>': `\x3e`,
+ '\\': `\\`,
+}
+
+// jsStrNormReplacementTable is like jsStrReplacementTable but does not
+// overencode existing escapes since this table has no entry for `\`.
+var jsStrNormReplacementTable = []string{
+ 0: `\0`,
+ '\t': `\t`,
+ '\n': `\n`,
+ '\v': `\x0b`, // "\v" == "v" on IE 6.
+ '\f': `\f`,
+ '\r': `\r`,
+ // Encode HTML specials as hex so the output can be embedded
+ // in HTML attributes without further encoding.
+ '"': `\x22`,
+ '&': `\x26`,
+ '\'': `\x27`,
+ '+': `\x2b`,
+ '/': `\/`,
+ '<': `\x3c`,
+ '>': `\x3e`,
+}
+
+var jsRegexpReplacementTable = []string{
+ 0: `\0`,
+ '\t': `\t`,
+ '\n': `\n`,
+ '\v': `\x0b`, // "\v" == "v" on IE 6.
+ '\f': `\f`,
+ '\r': `\r`,
+ // Encode HTML specials as hex so the output can be embedded
+ // in HTML attributes without further encoding.
+ '"': `\x22`,
+ '$': `\$`,
+ '&': `\x26`,
+ '\'': `\x27`,
+ '(': `\(`,
+ ')': `\)`,
+ '*': `\*`,
+ '+': `\x2b`,
+ '-': `\-`,
+ '.': `\.`,
+ '/': `\/`,
+ '<': `\x3c`,
+ '>': `\x3e`,
+ '?': `\?`,
+ '[': `\[`,
+ '\\': `\\`,
+ ']': `\]`,
+ '^': `\^`,
+ '{': `\{`,
+ '|': `\|`,
+ '}': `\}`,
+}
+
+// isJSIdentPart reports whether the given rune is a JS identifier part.
+// It does not handle all the non-Latin letters, joiners, and combining marks,
+// but it does handle every codepoint that can occur in a numeric literal or
+// a keyword.
+func isJSIdentPart(r rune) bool {
+ switch {
+ case r == '$':
+ return true
+ case '0' <= r && r <= '9':
+ return true
+ case 'A' <= r && r <= 'Z':
+ return true
+ case r == '_':
+ return true
+ case 'a' <= r && r <= 'z':
+ return true
+ }
+ return false
+}
diff --git a/template/js_test.go b/template/js_test.go
new file mode 100644
index 00000000..7af7997d
--- /dev/null
+++ b/template/js_test.go
@@ -0,0 +1,401 @@
+// Copyright 2011 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 template
+
+import (
+ "bytes"
+ "math"
+ "strings"
+ "testing"
+)
+
+func TestNextJsCtx(t *testing.T) {
+ tests := []struct {
+ jsCtx jsCtx
+ s string
+ }{
+ // Statement terminators precede regexps.
+ {jsCtxRegexp, ";"},
+ // This is not airtight.
+ // ({ valueOf: function () { return 1 } } / 2)
+ // is valid JavaScript but in practice, devs do not do this.
+ // A block followed by a statement starting with a RegExp is
+ // much more common:
+ // while (x) {...} /foo/.test(x) || panic()
+ {jsCtxRegexp, "}"},
+ // But member, call, grouping, and array expression terminators
+ // precede div ops.
+ {jsCtxDivOp, ")"},
+ {jsCtxDivOp, "]"},
+ // At the start of a primary expression, array, or expression
+ // statement, expect a regexp.
+ {jsCtxRegexp, "("},
+ {jsCtxRegexp, "["},
+ {jsCtxRegexp, "{"},
+ // Assignment operators precede regexps as do all exclusively
+ // prefix and binary operators.
+ {jsCtxRegexp, "="},
+ {jsCtxRegexp, "+="},
+ {jsCtxRegexp, "*="},
+ {jsCtxRegexp, "*"},
+ {jsCtxRegexp, "!"},
+ // Whether the + or - is infix or prefix, it cannot precede a
+ // div op.
+ {jsCtxRegexp, "+"},
+ {jsCtxRegexp, "-"},
+ // An incr/decr op precedes a div operator.
+ // This is not airtight. In (g = ++/h/i) a regexp follows a
+ // pre-increment operator, but in practice devs do not try to
+ // increment or decrement regular expressions.
+ // (g++/h/i) where ++ is a postfix operator on g is much more
+ // common.
+ {jsCtxDivOp, "--"},
+ {jsCtxDivOp, "++"},
+ {jsCtxDivOp, "x--"},
+ // When we have many dashes or pluses, then they are grouped
+ // left to right.
+ {jsCtxRegexp, "x---"}, // A postfix -- then a -.
+ // return followed by a slash returns the regexp literal or the
+ // slash starts a regexp literal in an expression statement that
+ // is dead code.
+ {jsCtxRegexp, "return"},
+ {jsCtxRegexp, "return "},
+ {jsCtxRegexp, "return\t"},
+ {jsCtxRegexp, "return\n"},
+ {jsCtxRegexp, "return\u2028"},
+ // Identifiers can be divided and cannot validly be preceded by
+ // a regular expressions. Semicolon insertion cannot happen
+ // between an identifier and a regular expression on a new line
+ // because the one token lookahead for semicolon insertion has
+ // to conclude that it could be a div binary op and treat it as
+ // such.
+ {jsCtxDivOp, "x"},
+ {jsCtxDivOp, "x "},
+ {jsCtxDivOp, "x\t"},
+ {jsCtxDivOp, "x\n"},
+ {jsCtxDivOp, "x\u2028"},
+ {jsCtxDivOp, "preturn"},
+ // Numbers precede div ops.
+ {jsCtxDivOp, "0"},
+ // Dots that are part of a number are div preceders.
+ {jsCtxDivOp, "0."},
+ }
+
+ for _, test := range tests {
+ if nextJSCtx([]byte(test.s), jsCtxRegexp) != test.jsCtx {
+ t.Errorf("want %s got %q", test.jsCtx, test.s)
+ }
+ if nextJSCtx([]byte(test.s), jsCtxDivOp) != test.jsCtx {
+ t.Errorf("want %s got %q", test.jsCtx, test.s)
+ }
+ }
+
+ if nextJSCtx([]byte(" "), jsCtxRegexp) != jsCtxRegexp {
+ t.Error("Blank tokens")
+ }
+
+ if nextJSCtx([]byte(" "), jsCtxDivOp) != jsCtxDivOp {
+ t.Error("Blank tokens")
+ }
+}
+
+func TestJSValEscaper(t *testing.T) {
+ tests := []struct {
+ x interface{}
+ js string
+ }{
+ {int(42), " 42 "},
+ {uint(42), " 42 "},
+ {int16(42), " 42 "},
+ {uint16(42), " 42 "},
+ {int32(-42), " -42 "},
+ {uint32(42), " 42 "},
+ {int16(-42), " -42 "},
+ {uint16(42), " 42 "},
+ {int64(-42), " -42 "},
+ {uint64(42), " 42 "},
+ {uint64(1) << 53, " 9007199254740992 "},
+ // ulp(1 << 53) > 1 so this loses precision in JS
+ // but it is still a representable integer literal.
+ {uint64(1)<<53 + 1, " 9007199254740993 "},
+ {float32(1.0), " 1 "},
+ {float32(-1.0), " -1 "},
+ {float32(0.5), " 0.5 "},
+ {float32(-0.5), " -0.5 "},
+ {float32(1.0) / float32(256), " 0.00390625 "},
+ {float32(0), " 0 "},
+ {math.Copysign(0, -1), " -0 "},
+ {float64(1.0), " 1 "},
+ {float64(-1.0), " -1 "},
+ {float64(0.5), " 0.5 "},
+ {float64(-0.5), " -0.5 "},
+ {float64(0), " 0 "},
+ {math.Copysign(0, -1), " -0 "},
+ {"", `""`},
+ {"foo", `"foo"`},
+ // Newlines.
+ {"\r\n\u2028\u2029", `"\r\n\u2028\u2029"`},
+ // "\v" == "v" on IE 6 so use "\x0b" instead.
+ {"\t\x0b", `"\t\u000b"`},
+ {struct{ X, Y int }{1, 2}, `{"X":1,"Y":2}`},
+ {[]interface{}{}, "[]"},
+ {[]interface{}{42, "foo", nil}, `[42,"foo",null]`},
+ {[]string{""}, `["\u003c!--","\u003c/script\u003e","--\u003e"]`},
+ {"", `"--\u003e"`},
+ {"", `"]]\u003e"`},
+ {"", `\x3c\/script\x3e`},
+ {"", `]]\x3e`},
+ // http://dev.w3.org/html5/markup/aria/syntax.html#escaping-text-span
+ // "The text in style, script, title, and textarea elements
+ // must not have an escaping text span start that is not
+ // followed by an escaping text span end."
+ // Furthermore, spoofing an escaping text span end could lead
+ // to different interpretation of a sequence otherwise
+ // masked by the escaping text span, and spoofing a start could
+ // allow regular text content to be interpreted as script
+ // allowing script execution via a combination of a JS string
+ // injection followed by an HTML text injection.
+ {"", `--\x3e`},
+ // From http://code.google.com/p/doctype/wiki/ArticleUtf7
+ {"+ADw-script+AD4-alert(1)+ADw-/script+AD4-",
+ `\x2bADw-script\x2bAD4-alert(1)\x2bADw-\/script\x2bAD4-`,
+ },
+ // Invalid UTF-8 sequence
+ {"foo\xA0bar", "foo\xA0bar"},
+ // Invalid unicode scalar value.
+ {"foo\xed\xa0\x80bar", "foo\xed\xa0\x80bar"},
+ }
+
+ for _, test := range tests {
+ esc := jsStrEscaper(test.x)
+ if esc != test.esc {
+ t.Errorf("%q: want %q got %q", test.x, test.esc, esc)
+ }
+ }
+}
+
+func TestJSRegexpEscaper(t *testing.T) {
+ tests := []struct {
+ x interface{}
+ esc string
+ }{
+ {"", `(?:)`},
+ {"foo", `foo`},
+ {"\u0000", `\0`},
+ {"\t", `\t`},
+ {"\n", `\n`},
+ {"\r", `\r`},
+ {"\u2028", `\u2028`},
+ {"\u2029", `\u2029`},
+ {"\\", `\\`},
+ {"\\n", `\\n`},
+ {"foo\r\nbar", `foo\r\nbar`},
+ // Preserve attribute boundaries.
+ {`"`, `\x22`},
+ {`'`, `\x27`},
+ // Allow embedding in HTML without further escaping.
+ {`&`, `\x26amp;`},
+ // Prevent breaking out of text node and element boundaries.
+ {"", `\x3c\/script\x3e`},
+ {"", `\]\]\x3e`},
+ // Escaping text spans.
+ {"", `\-\-\x3e`},
+ {"*", `\*`},
+ {"+", `\x2b`},
+ {"?", `\?`},
+ {"[](){}", `\[\]\(\)\{\}`},
+ {"$foo|x.y", `\$foo\|x\.y`},
+ {"x^y", `x\^y`},
+ }
+
+ for _, test := range tests {
+ esc := jsRegexpEscaper(test.x)
+ if esc != test.esc {
+ t.Errorf("%q: want %q got %q", test.x, test.esc, esc)
+ }
+ }
+}
+
+func TestEscapersOnLower7AndSelectHighCodepoints(t *testing.T) {
+ input := ("\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" +
+ "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
+ ` !"#$%&'()*+,-./` +
+ `0123456789:;<=>?` +
+ `@ABCDEFGHIJKLMNO` +
+ `PQRSTUVWXYZ[\]^_` +
+ "`abcdefghijklmno" +
+ "pqrstuvwxyz{|}~\x7f" +
+ "\u00A0\u0100\u2028\u2029\ufeff\U0001D11E")
+
+ tests := []struct {
+ name string
+ escaper func(...interface{}) string
+ escaped string
+ }{
+ {
+ "jsStrEscaper",
+ jsStrEscaper,
+ "\\0\x01\x02\x03\x04\x05\x06\x07" +
+ "\x08\\t\\n\\x0b\\f\\r\x0E\x0F" +
+ "\x10\x11\x12\x13\x14\x15\x16\x17" +
+ "\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
+ ` !\x22#$%\x26\x27()*\x2b,-.\/` +
+ `0123456789:;\x3c=\x3e?` +
+ `@ABCDEFGHIJKLMNO` +
+ `PQRSTUVWXYZ[\\]^_` +
+ "`abcdefghijklmno" +
+ "pqrstuvwxyz{|}~\x7f" +
+ "\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E",
+ },
+ {
+ "jsRegexpEscaper",
+ jsRegexpEscaper,
+ "\\0\x01\x02\x03\x04\x05\x06\x07" +
+ "\x08\\t\\n\\x0b\\f\\r\x0E\x0F" +
+ "\x10\x11\x12\x13\x14\x15\x16\x17" +
+ "\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
+ ` !\x22#\$%\x26\x27\(\)\*\x2b,\-\.\/` +
+ `0123456789:;\x3c=\x3e\?` +
+ `@ABCDEFGHIJKLMNO` +
+ `PQRSTUVWXYZ\[\\\]\^_` +
+ "`abcdefghijklmno" +
+ `pqrstuvwxyz\{\|\}~` + "\u007f" +
+ "\u00A0\u0100\\u2028\\u2029\ufeff\U0001D11E",
+ },
+ }
+
+ for _, test := range tests {
+ if s := test.escaper(input); s != test.escaped {
+ t.Errorf("%s once: want\n\t%q\ngot\n\t%q", test.name, test.escaped, s)
+ continue
+ }
+
+ // Escape it rune by rune to make sure that any
+ // fast-path checking does not break escaping.
+ var buf bytes.Buffer
+ for _, c := range input {
+ buf.WriteString(test.escaper(string(c)))
+ }
+
+ if s := buf.String(); s != test.escaped {
+ t.Errorf("%s rune-wise: want\n\t%q\ngot\n\t%q", test.name, test.escaped, s)
+ continue
+ }
+ }
+}
+
+func BenchmarkJSValEscaperWithNum(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ jsValEscaper(3.141592654)
+ }
+}
+
+func BenchmarkJSValEscaperWithStr(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ jsValEscaper("The
quick,\r\n
brown fox jumps\u2028over the
dog")
+ }
+}
+
+func BenchmarkJSValEscaperWithStrNoSpecials(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ jsValEscaper("The quick, brown fox jumps over the lazy dog")
+ }
+}
+
+func BenchmarkJSValEscaperWithObj(b *testing.B) {
+ o := struct {
+ S string
+ N int
+ }{
+ "The
quick,\r\n
brown fox jumps\u2028over the
dog\u2028",
+ 42,
+ }
+ for i := 0; i < b.N; i++ {
+ jsValEscaper(o)
+ }
+}
+
+func BenchmarkJSValEscaperWithObjNoSpecials(b *testing.B) {
+ o := struct {
+ S string
+ N int
+ }{
+ "The quick, brown fox jumps over the lazy dog",
+ 42,
+ }
+ for i := 0; i < b.N; i++ {
+ jsValEscaper(o)
+ }
+}
+
+func BenchmarkJSStrEscaperNoSpecials(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ jsStrEscaper("The quick, brown fox jumps over the lazy dog.")
+ }
+}
+
+func BenchmarkJSStrEscaper(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ jsStrEscaper("The
quick,\r\n
brown fox jumps\u2028over the
dog")
+ }
+}
+
+func BenchmarkJSRegexpEscaperNoSpecials(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ jsRegexpEscaper("The quick, brown fox jumps over the lazy dog")
+ }
+}
+
+func BenchmarkJSRegexpEscaper(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ jsRegexpEscaper("The
quick,\r\n
brown fox jumps\u2028over the
dog")
+ }
+}
diff --git a/template/template.go b/template/template.go
new file mode 100644
index 00000000..65b5bff5
--- /dev/null
+++ b/template/template.go
@@ -0,0 +1,390 @@
+// Copyright 2011 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 template
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "path/filepath"
+ "sync"
+ "text/template"
+ "text/template/parse"
+)
+
+// Template is a specialized Template from "text/template" that produces a safe
+// HTML document fragment.
+type Template struct {
+ // Sticky error if escaping fails.
+ escapeErr error
+ // We could embed the text/template field, but it's safer not to because
+ // we need to keep our version of the name space and the underlying
+ // template's in sync.
+ text *template.Template
+ // The underlying template's parse tree, updated to be HTML-safe.
+ Tree *parse.Tree
+ *nameSpace // common to all associated templates
+}
+
+// escapeOK is a sentinel value used to indicate valid escaping.
+var escapeOK = fmt.Errorf("template escaped correctly")
+
+// nameSpace is the data structure shared by all templates in an association.
+type nameSpace struct {
+ mu sync.Mutex
+ set map[string]*Template
+}
+
+// Templates returns a slice of the templates associated with t, including t
+// itself.
+func (t *Template) Templates() []*Template {
+ ns := t.nameSpace
+ ns.mu.Lock()
+ defer ns.mu.Unlock()
+ // Return a slice so we don't expose the map.
+ m := make([]*Template, 0, len(ns.set))
+ for _, v := range ns.set {
+ m = append(m, v)
+ }
+ return m
+}
+
+// escape escapes all associated templates.
+func (t *Template) escape() error {
+ t.nameSpace.mu.Lock()
+ defer t.nameSpace.mu.Unlock()
+ if t.escapeErr == nil {
+ if err := escapeTemplate(t, t.text.Root, t.Name()); err != nil {
+ return err
+ }
+ } else if t.escapeErr != escapeOK {
+ return t.escapeErr
+ }
+ return nil
+}
+
+// Execute applies a parsed template to the specified data object,
+// writing the output to wr.
+// If an error occurs executing the template or writing its output,
+// execution stops, but partial results may already have been written to
+// the output writer.
+// A template may be executed safely in parallel.
+func (t *Template) Execute(wr io.Writer, data interface{}) error {
+ if err := t.escape(); err != nil {
+ return err
+ }
+ return t.text.Execute(wr, data)
+}
+
+// ExecuteTemplate applies the template associated with t that has the given
+// name to the specified data object and writes the output to wr.
+// If an error occurs executing the template or writing its output,
+// execution stops, but partial results may already have been written to
+// the output writer.
+// A template may be executed safely in parallel.
+func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error {
+ tmpl, err := t.lookupAndEscapeTemplate(name)
+ if err != nil {
+ return err
+ }
+ return tmpl.text.Execute(wr, data)
+}
+
+// lookupAndEscapeTemplate guarantees that the template with the given name
+// is escaped, or returns an error if it cannot be. It returns the named
+// template.
+func (t *Template) lookupAndEscapeTemplate(name string) (tmpl *Template, err error) {
+ t.nameSpace.mu.Lock()
+ defer t.nameSpace.mu.Unlock()
+ tmpl = t.set[name]
+ if tmpl == nil {
+ return nil, fmt.Errorf("html/template: %q is undefined", name)
+ }
+ if tmpl.escapeErr != nil && tmpl.escapeErr != escapeOK {
+ return nil, tmpl.escapeErr
+ }
+ if tmpl.text.Tree == nil || tmpl.text.Root == nil {
+ return nil, fmt.Errorf("html/template: %q is an incomplete template", name)
+ }
+ if t.text.Lookup(name) == nil {
+ panic("html/template internal error: template escaping out of sync")
+ }
+ if tmpl.escapeErr == nil {
+ err = escapeTemplate(tmpl, tmpl.text.Root, name)
+ }
+ return tmpl, err
+}
+
+// Parse parses a string into a template. Nested template definitions
+// will be associated with the top-level template t. Parse may be
+// called multiple times to parse definitions of templates to associate
+// with t. It is an error if a resulting template is non-empty (contains
+// content other than template definitions) and would replace a
+// non-empty template with the same name. (In multiple calls to Parse
+// with the same receiver template, only one call can contain text
+// other than space, comments, and template definitions.)
+func (t *Template) Parse(src string) (*Template, error) {
+ t.nameSpace.mu.Lock()
+ t.escapeErr = nil
+ t.nameSpace.mu.Unlock()
+ ret, err := t.text.Parse(src)
+ if err != nil {
+ return nil, err
+ }
+ // In general, all the named templates might have changed underfoot.
+ // Regardless, some new ones may have been defined.
+ // The template.Template set has been updated; update ours.
+ t.nameSpace.mu.Lock()
+ defer t.nameSpace.mu.Unlock()
+ for _, v := range ret.Templates() {
+ name := v.Name()
+ tmpl := t.set[name]
+ if tmpl == nil {
+ tmpl = t.new(name)
+ }
+ // Restore our record of this text/template to its unescaped original state.
+ tmpl.escapeErr = nil
+ tmpl.text = v
+ tmpl.Tree = v.Tree
+ }
+ return t, nil
+}
+
+// AddParseTree creates a new template with the name and parse tree
+// and associates it with t.
+//
+// It returns an error if t has already been executed.
+func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) {
+ t.nameSpace.mu.Lock()
+ defer t.nameSpace.mu.Unlock()
+ if t.escapeErr != nil {
+ return nil, fmt.Errorf("html/template: cannot AddParseTree to %q after it has executed", t.Name())
+ }
+ text, err := t.text.AddParseTree(name, tree)
+ if err != nil {
+ return nil, err
+ }
+ ret := &Template{
+ nil,
+ text,
+ text.Tree,
+ t.nameSpace,
+ }
+ t.set[name] = ret
+ return ret, nil
+}
+
+// Clone returns a duplicate of the template, including all associated
+// templates. The actual representation is not copied, but the name space of
+// associated templates is, so further calls to Parse in the copy will add
+// templates to the copy but not to the original. Clone can be used to prepare
+// common templates and use them with variant definitions for other templates
+// by adding the variants after the clone is made.
+//
+// It returns an error if t has already been executed.
+func (t *Template) Clone() (*Template, error) {
+ t.nameSpace.mu.Lock()
+ defer t.nameSpace.mu.Unlock()
+ if t.escapeErr != nil {
+ return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed", t.Name())
+ }
+ textClone, err := t.text.Clone()
+ if err != nil {
+ return nil, err
+ }
+ ret := &Template{
+ nil,
+ textClone,
+ textClone.Tree,
+ &nameSpace{
+ set: make(map[string]*Template),
+ },
+ }
+ for _, x := range textClone.Templates() {
+ name := x.Name()
+ src := t.set[name]
+ if src == nil || src.escapeErr != nil {
+ return nil, fmt.Errorf("html/template: cannot Clone %q after it has executed", t.Name())
+ }
+ x.Tree = x.Tree.Copy()
+ ret.set[name] = &Template{
+ nil,
+ x,
+ x.Tree,
+ ret.nameSpace,
+ }
+ }
+ return ret, nil
+}
+
+// New allocates a new HTML template with the given name.
+func New(name string) *Template {
+ tmpl := &Template{
+ nil,
+ template.New(name),
+ nil,
+ &nameSpace{
+ set: make(map[string]*Template),
+ },
+ }
+ tmpl.set[name] = tmpl
+ return tmpl
+}
+
+// New allocates a new HTML template associated with the given one
+// and with the same delimiters. The association, which is transitive,
+// allows one template to invoke another with a {{template}} action.
+func (t *Template) New(name string) *Template {
+ t.nameSpace.mu.Lock()
+ defer t.nameSpace.mu.Unlock()
+ return t.new(name)
+}
+
+// new is the implementation of New, without the lock.
+func (t *Template) new(name string) *Template {
+ tmpl := &Template{
+ nil,
+ t.text.New(name),
+ nil,
+ t.nameSpace,
+ }
+ tmpl.set[name] = tmpl
+ return tmpl
+}
+
+// Name returns the name of the template.
+func (t *Template) Name() string {
+ return t.text.Name()
+}
+
+// FuncMap is the type of the map defining the mapping from names to
+// functions. Each function must have either a single return value, or two
+// return values of which the second has type error. In that case, if the
+// second (error) argument evaluates to non-nil during execution, execution
+// terminates and Execute returns that error. FuncMap has the same base type
+// as FuncMap in "text/template", copied here so clients need not import
+// "text/template".
+type FuncMap map[string]interface{}
+
+// Funcs adds the elements of the argument map to the template's function map.
+// It panics if a value in the map is not a function with appropriate return
+// type. However, it is legal to overwrite elements of the map. The return
+// value is the template, so calls can be chained.
+func (t *Template) Funcs(funcMap FuncMap) *Template {
+ t.text.Funcs(template.FuncMap(funcMap))
+ return t
+}
+
+// Delims sets the action delimiters to the specified strings, to be used in
+// subsequent calls to Parse, ParseFiles, or ParseGlob. Nested template
+// definitions will inherit the settings. An empty delimiter stands for the
+// corresponding default: {{ or }}.
+// The return value is the template, so calls can be chained.
+func (t *Template) Delims(left, right string) *Template {
+ t.text.Delims(left, right)
+ return t
+}
+
+// Lookup returns the template with the given name that is associated with t,
+// or nil if there is no such template.
+func (t *Template) Lookup(name string) *Template {
+ t.nameSpace.mu.Lock()
+ defer t.nameSpace.mu.Unlock()
+ return t.set[name]
+}
+
+// Must is a helper that wraps a call to a function returning (*Template, error)
+// and panics if the error is non-nil. It is intended for use in variable initializations
+// such as
+// var t = template.Must(template.New("name").Parse("html"))
+func Must(t *Template, err error) *Template {
+ if err != nil {
+ panic(err)
+ }
+ return t
+}
+
+// ParseFiles creates a new Template and parses the template definitions from
+// the named files. The returned template's name will have the (base) name and
+// (parsed) contents of the first file. There must be at least one file.
+// If an error occurs, parsing stops and the returned *Template is nil.
+func ParseFiles(filenames ...string) (*Template, error) {
+ return parseFiles(nil, filenames...)
+}
+
+// ParseFiles parses the named files and associates the resulting templates with
+// t. If an error occurs, parsing stops and the returned template is nil;
+// otherwise it is t. There must be at least one file.
+func (t *Template) ParseFiles(filenames ...string) (*Template, error) {
+ return parseFiles(t, filenames...)
+}
+
+// parseFiles is the helper for the method and function. If the argument
+// template is nil, it is created from the first file.
+func parseFiles(t *Template, filenames ...string) (*Template, error) {
+ if len(filenames) == 0 {
+ // Not really a problem, but be consistent.
+ return nil, fmt.Errorf("html/template: no files named in call to ParseFiles")
+ }
+ for _, filename := range filenames {
+ b, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return nil, err
+ }
+ s := string(b)
+ // name := filepath.Base(filename)
+ name := filename
+ // First template becomes return value if not already defined,
+ // and we use that one for subsequent New calls to associate
+ // all the templates together. Also, if this file has the same name
+ // as t, this file becomes the contents of t, so
+ // t, err := New(name).Funcs(xxx).ParseFiles(name)
+ // works. Otherwise we create a new template associated with t.
+ var tmpl *Template
+ if t == nil {
+ t = New(name)
+ }
+ if name == t.Name() {
+ tmpl = t
+ } else {
+ tmpl = t.New(name)
+ }
+ _, err = tmpl.Parse(s)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return t, nil
+}
+
+// ParseGlob creates a new Template and parses the template definitions from the
+// files identified by the pattern, which must match at least one file. The
+// returned template will have the (base) name and (parsed) contents of the
+// first file matched by the pattern. ParseGlob is equivalent to calling
+// ParseFiles with the list of files matched by the pattern.
+func ParseGlob(pattern string) (*Template, error) {
+ return parseGlob(nil, pattern)
+}
+
+// ParseGlob parses the template definitions in the files identified by the
+// pattern and associates the resulting templates with t. The pattern is
+// processed by filepath.Glob and must match at least one file. ParseGlob is
+// equivalent to calling t.ParseFiles with the list of files matched by the
+// pattern.
+func (t *Template) ParseGlob(pattern string) (*Template, error) {
+ return parseGlob(t, pattern)
+}
+
+// parseGlob is the implementation of the function and method ParseGlob.
+func parseGlob(t *Template, pattern string) (*Template, error) {
+ filenames, err := filepath.Glob(pattern)
+ if err != nil {
+ return nil, err
+ }
+ if len(filenames) == 0 {
+ return nil, fmt.Errorf("html/template: pattern matches no files: %#q", pattern)
+ }
+ return parseFiles(t, filenames...)
+}
diff --git a/template/transition.go b/template/transition.go
new file mode 100644
index 00000000..b486fcd2
--- /dev/null
+++ b/template/transition.go
@@ -0,0 +1,550 @@
+// Copyright 2011 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 template
+
+import (
+ "bytes"
+ "strings"
+)
+
+// transitionFunc is the array of context transition functions for text nodes.
+// A transition function takes a context and template text input, and returns
+// the updated context and the number of bytes consumed from the front of the
+// input.
+var transitionFunc = [...]func(context, []byte) (context, int){
+ stateText: tText,
+ stateTag: tTag,
+ stateAttrName: tAttrName,
+ stateAfterName: tAfterName,
+ stateBeforeValue: tBeforeValue,
+ stateHTMLCmt: tHTMLCmt,
+ stateRCDATA: tSpecialTagEnd,
+ stateAttr: tAttr,
+ stateURL: tURL,
+ stateJS: tJS,
+ stateJSDqStr: tJSDelimited,
+ stateJSSqStr: tJSDelimited,
+ stateJSRegexp: tJSDelimited,
+ stateJSBlockCmt: tBlockCmt,
+ stateJSLineCmt: tLineCmt,
+ stateCSS: tCSS,
+ stateCSSDqStr: tCSSStr,
+ stateCSSSqStr: tCSSStr,
+ stateCSSDqURL: tCSSStr,
+ stateCSSSqURL: tCSSStr,
+ stateCSSURL: tCSSStr,
+ stateCSSBlockCmt: tBlockCmt,
+ stateCSSLineCmt: tLineCmt,
+ stateError: tError,
+}
+
+var commentStart = []byte("")
+
+// tText is the context transition function for the text state.
+func tText(c context, s []byte) (context, int) {
+ k := 0
+ for {
+ i := k + bytes.IndexByte(s[k:], '<')
+ if i < k || i+1 == len(s) {
+ return c, len(s)
+ } else if i+4 <= len(s) && bytes.Equal(commentStart, s[i:i+4]) {
+ return context{state: stateHTMLCmt}, i + 4
+ }
+ i++
+ end := false
+ if s[i] == '/' {
+ if i+1 == len(s) {
+ return c, len(s)
+ }
+ end, i = true, i+1
+ }
+ j, e := eatTagName(s, i)
+ if j != i {
+ if end {
+ e = elementNone
+ }
+ // We've found an HTML tag.
+ return context{state: stateTag, element: e}, j
+ }
+ k = j
+ }
+}
+
+var elementContentType = [...]state{
+ elementNone: stateText,
+ elementScript: stateJS,
+ elementStyle: stateCSS,
+ elementTextarea: stateRCDATA,
+ elementTitle: stateRCDATA,
+}
+
+// tTag is the context transition function for the tag state.
+func tTag(c context, s []byte) (context, int) {
+ // Find the attribute name.
+ i := eatWhiteSpace(s, 0)
+ if i == len(s) {
+ return c, len(s)
+ }
+ if s[i] == '>' {
+ return context{
+ state: elementContentType[c.element],
+ element: c.element,
+ }, i + 1
+ }
+ j, err := eatAttrName(s, i)
+ if err != nil {
+ return context{state: stateError, err: err}, len(s)
+ }
+ state, attr := stateTag, attrNone
+ if i == j {
+ return context{
+ state: stateError,
+ err: errorf(ErrBadHTML, nil, 0, "expected space, attr name, or end of tag, but got %q", s[i:]),
+ }, len(s)
+ }
+ switch attrType(string(s[i:j])) {
+ case contentTypeURL:
+ attr = attrURL
+ case contentTypeCSS:
+ attr = attrStyle
+ case contentTypeJS:
+ attr = attrScript
+ }
+ if j == len(s) {
+ state = stateAttrName
+ } else {
+ state = stateAfterName
+ }
+ return context{state: state, element: c.element, attr: attr}, j
+}
+
+// tAttrName is the context transition function for stateAttrName.
+func tAttrName(c context, s []byte) (context, int) {
+ i, err := eatAttrName(s, 0)
+ if err != nil {
+ return context{state: stateError, err: err}, len(s)
+ } else if i != len(s) {
+ c.state = stateAfterName
+ }
+ return c, i
+}
+
+// tAfterName is the context transition function for stateAfterName.
+func tAfterName(c context, s []byte) (context, int) {
+ // Look for the start of the value.
+ i := eatWhiteSpace(s, 0)
+ if i == len(s) {
+ return c, len(s)
+ } else if s[i] != '=' {
+ // Occurs due to tag ending '>', and valueless attribute.
+ c.state = stateTag
+ return c, i
+ }
+ c.state = stateBeforeValue
+ // Consume the "=".
+ return c, i + 1
+}
+
+var attrStartStates = [...]state{
+ attrNone: stateAttr,
+ attrScript: stateJS,
+ attrStyle: stateCSS,
+ attrURL: stateURL,
+}
+
+// tBeforeValue is the context transition function for stateBeforeValue.
+func tBeforeValue(c context, s []byte) (context, int) {
+ i := eatWhiteSpace(s, 0)
+ if i == len(s) {
+ return c, len(s)
+ }
+ // Find the attribute delimiter.
+ delim := delimSpaceOrTagEnd
+ switch s[i] {
+ case '\'':
+ delim, i = delimSingleQuote, i+1
+ case '"':
+ delim, i = delimDoubleQuote, i+1
+ }
+ c.state, c.delim, c.attr = attrStartStates[c.attr], delim, attrNone
+ return c, i
+}
+
+// tHTMLCmt is the context transition function for stateHTMLCmt.
+func tHTMLCmt(c context, s []byte) (context, int) {
+ if i := bytes.Index(s, commentEnd); i != -1 {
+ return context{}, i + 3
+ }
+ return c, len(s)
+}
+
+// specialTagEndMarkers maps element types to the character sequence that
+// case-insensitively signals the end of the special tag body.
+var specialTagEndMarkers = [...]string{
+ elementScript: "= 0 {
+ c.urlPart = urlPartQueryOrFrag
+ } else if len(s) != eatWhiteSpace(s, 0) && c.urlPart == urlPartNone {
+ // HTML5 uses "Valid URL potentially surrounded by spaces" for
+ // attrs: http://www.w3.org/TR/html5/index.html#attributes-1
+ c.urlPart = urlPartPreQuery
+ }
+ return c, len(s)
+}
+
+// tJS is the context transition function for the JS state.
+func tJS(c context, s []byte) (context, int) {
+ i := bytes.IndexAny(s, `"'/`)
+ if i == -1 {
+ // Entire input is non string, comment, regexp tokens.
+ c.jsCtx = nextJSCtx(s, c.jsCtx)
+ return c, len(s)
+ }
+ c.jsCtx = nextJSCtx(s[:i], c.jsCtx)
+ switch s[i] {
+ case '"':
+ c.state, c.jsCtx = stateJSDqStr, jsCtxRegexp
+ case '\'':
+ c.state, c.jsCtx = stateJSSqStr, jsCtxRegexp
+ case '/':
+ switch {
+ case i+1 < len(s) && s[i+1] == '/':
+ c.state, i = stateJSLineCmt, i+1
+ case i+1 < len(s) && s[i+1] == '*':
+ c.state, i = stateJSBlockCmt, i+1
+ case c.jsCtx == jsCtxRegexp:
+ c.state = stateJSRegexp
+ case c.jsCtx == jsCtxDivOp:
+ c.jsCtx = jsCtxRegexp
+ default:
+ return context{
+ state: stateError,
+ err: errorf(ErrSlashAmbig, nil, 0, "'/' could start a division or regexp: %.32q", s[i:]),
+ }, len(s)
+ }
+ default:
+ panic("unreachable")
+ }
+ return c, i + 1
+}
+
+// tJSDelimited is the context transition function for the JS string and regexp
+// states.
+func tJSDelimited(c context, s []byte) (context, int) {
+ specials := `\"`
+ switch c.state {
+ case stateJSSqStr:
+ specials = `\'`
+ case stateJSRegexp:
+ specials = `\/[]`
+ }
+
+ k, inCharset := 0, false
+ for {
+ i := k + bytes.IndexAny(s[k:], specials)
+ if i < k {
+ break
+ }
+ switch s[i] {
+ case '\\':
+ i++
+ if i == len(s) {
+ return context{
+ state: stateError,
+ err: errorf(ErrPartialEscape, nil, 0, "unfinished escape sequence in JS string: %q", s),
+ }, len(s)
+ }
+ case '[':
+ inCharset = true
+ case ']':
+ inCharset = false
+ default:
+ // end delimiter
+ if !inCharset {
+ c.state, c.jsCtx = stateJS, jsCtxDivOp
+ return c, i + 1
+ }
+ }
+ k = i + 1
+ }
+
+ if inCharset {
+ // This can be fixed by making context richer if interpolation
+ // into charsets is desired.
+ return context{
+ state: stateError,
+ err: errorf(ErrPartialCharset, nil, 0, "unfinished JS regexp charset: %q", s),
+ }, len(s)
+ }
+
+ return c, len(s)
+}
+
+var blockCommentEnd = []byte("*/")
+
+// tBlockCmt is the context transition function for /*comment*/ states.
+func tBlockCmt(c context, s []byte) (context, int) {
+ i := bytes.Index(s, blockCommentEnd)
+ if i == -1 {
+ return c, len(s)
+ }
+ switch c.state {
+ case stateJSBlockCmt:
+ c.state = stateJS
+ case stateCSSBlockCmt:
+ c.state = stateCSS
+ default:
+ panic(c.state.String())
+ }
+ return c, i + 2
+}
+
+// tLineCmt is the context transition function for //comment states.
+func tLineCmt(c context, s []byte) (context, int) {
+ var lineTerminators string
+ var endState state
+ switch c.state {
+ case stateJSLineCmt:
+ lineTerminators, endState = "\n\r\u2028\u2029", stateJS
+ case stateCSSLineCmt:
+ lineTerminators, endState = "\n\f\r", stateCSS
+ // Line comments are not part of any published CSS standard but
+ // are supported by the 4 major browsers.
+ // This defines line comments as
+ // LINECOMMENT ::= "//" [^\n\f\d]*
+ // since http://www.w3.org/TR/css3-syntax/#SUBTOK-nl defines
+ // newlines:
+ // nl ::= #xA | #xD #xA | #xD | #xC
+ default:
+ panic(c.state.String())
+ }
+
+ i := bytes.IndexAny(s, lineTerminators)
+ if i == -1 {
+ return c, len(s)
+ }
+ c.state = endState
+ // Per section 7.4 of EcmaScript 5 : http://es5.github.com/#x7.4
+ // "However, the LineTerminator at the end of the line is not
+ // considered to be part of the single-line comment; it is
+ // recognized separately by the lexical grammar and becomes part
+ // of the stream of input elements for the syntactic grammar."
+ return c, i
+}
+
+// tCSS is the context transition function for the CSS state.
+func tCSS(c context, s []byte) (context, int) {
+ // CSS quoted strings are almost never used except for:
+ // (1) URLs as in background: "/foo.png"
+ // (2) Multiword font-names as in font-family: "Times New Roman"
+ // (3) List separators in content values as in inline-lists:
+ //
+ //
+ // (4) Attribute value selectors as in a[href="http://example.com/"]
+ //
+ // We conservatively treat all strings as URLs, but make some
+ // allowances to avoid confusion.
+ //
+ // In (1), our conservative assumption is justified.
+ // In (2), valid font names do not contain ':', '?', or '#', so our
+ // conservative assumption is fine since we will never transition past
+ // urlPartPreQuery.
+ // In (3), our protocol heuristic should not be tripped, and there
+ // should not be non-space content after a '?' or '#', so as long as
+ // we only %-encode RFC 3986 reserved characters we are ok.
+ // In (4), we should URL escape for URL attributes, and for others we
+ // have the attribute name available if our conservative assumption
+ // proves problematic for real code.
+
+ k := 0
+ for {
+ i := k + bytes.IndexAny(s[k:], `("'/`)
+ if i < k {
+ return c, len(s)
+ }
+ switch s[i] {
+ case '(':
+ // Look for url to the left.
+ p := bytes.TrimRight(s[:i], "\t\n\f\r ")
+ if endsWithCSSKeyword(p, "url") {
+ j := len(s) - len(bytes.TrimLeft(s[i+1:], "\t\n\f\r "))
+ switch {
+ case j != len(s) && s[j] == '"':
+ c.state, j = stateCSSDqURL, j+1
+ case j != len(s) && s[j] == '\'':
+ c.state, j = stateCSSSqURL, j+1
+ default:
+ c.state = stateCSSURL
+ }
+ return c, j
+ }
+ case '/':
+ if i+1 < len(s) {
+ switch s[i+1] {
+ case '/':
+ c.state = stateCSSLineCmt
+ return c, i + 2
+ case '*':
+ c.state = stateCSSBlockCmt
+ return c, i + 2
+ }
+ }
+ case '"':
+ c.state = stateCSSDqStr
+ return c, i + 1
+ case '\'':
+ c.state = stateCSSSqStr
+ return c, i + 1
+ }
+ k = i + 1
+ }
+}
+
+// tCSSStr is the context transition function for the CSS string and URL states.
+func tCSSStr(c context, s []byte) (context, int) {
+ var endAndEsc string
+ switch c.state {
+ case stateCSSDqStr, stateCSSDqURL:
+ endAndEsc = `\"`
+ case stateCSSSqStr, stateCSSSqURL:
+ endAndEsc = `\'`
+ case stateCSSURL:
+ // Unquoted URLs end with a newline or close parenthesis.
+ // The below includes the wc (whitespace character) and nl.
+ endAndEsc = "\\\t\n\f\r )"
+ default:
+ panic(c.state.String())
+ }
+
+ k := 0
+ for {
+ i := k + bytes.IndexAny(s[k:], endAndEsc)
+ if i < k {
+ c, nread := tURL(c, decodeCSS(s[k:]))
+ return c, k + nread
+ }
+ if s[i] == '\\' {
+ i++
+ if i == len(s) {
+ return context{
+ state: stateError,
+ err: errorf(ErrPartialEscape, nil, 0, "unfinished escape sequence in CSS string: %q", s),
+ }, len(s)
+ }
+ } else {
+ c.state = stateCSS
+ return c, i + 1
+ }
+ c, _ = tURL(c, decodeCSS(s[:i+1]))
+ k = i + 1
+ }
+}
+
+// tError is the context transition function for the error state.
+func tError(c context, s []byte) (context, int) {
+ return c, len(s)
+}
+
+// eatAttrName returns the largest j such that s[i:j] is an attribute name.
+// It returns an error if s[i:] does not look like it begins with an
+// attribute name, such as encountering a quote mark without a preceding
+// equals sign.
+func eatAttrName(s []byte, i int) (int, *Error) {
+ for j := i; j < len(s); j++ {
+ switch s[j] {
+ case ' ', '\t', '\n', '\f', '\r', '=', '>':
+ return j, nil
+ case '\'', '"', '<':
+ // These result in a parse warning in HTML5 and are
+ // indicative of serious problems if seen in an attr
+ // name in a template.
+ return -1, errorf(ErrBadHTML, nil, 0, "%q in attribute name: %.32q", s[j:j+1], s)
+ default:
+ // No-op.
+ }
+ }
+ return len(s), nil
+}
+
+var elementNameMap = map[string]element{
+ "script": elementScript,
+ "style": elementStyle,
+ "textarea": elementTextarea,
+ "title": elementTitle,
+}
+
+// asciiAlpha reports whether c is an ASCII letter.
+func asciiAlpha(c byte) bool {
+ return 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z'
+}
+
+// asciiAlphaNum reports whether c is an ASCII letter or digit.
+func asciiAlphaNum(c byte) bool {
+ return asciiAlpha(c) || '0' <= c && c <= '9'
+}
+
+// eatTagName returns the largest j such that s[i:j] is a tag name and the tag type.
+func eatTagName(s []byte, i int) (int, element) {
+ if i == len(s) || !asciiAlpha(s[i]) {
+ return i, elementNone
+ }
+ j := i + 1
+ for j < len(s) {
+ x := s[j]
+ if asciiAlphaNum(x) {
+ j++
+ continue
+ }
+ // Allow "x-y" or "x:y" but not "x-", "-y", or "x--y".
+ if (x == ':' || x == '-') && j+1 < len(s) && asciiAlphaNum(s[j+1]) {
+ j += 2
+ continue
+ }
+ break
+ }
+ return j, elementNameMap[strings.ToLower(string(s[i:j]))]
+}
+
+// eatWhiteSpace returns the largest j such that s[i:j] is white space.
+func eatWhiteSpace(s []byte, i int) int {
+ for j := i; j < len(s); j++ {
+ switch s[j] {
+ case ' ', '\t', '\n', '\f', '\r':
+ // No-op.
+ default:
+ return j
+ }
+ }
+ return len(s)
+}
diff --git a/template/url.go b/template/url.go
new file mode 100644
index 00000000..2ca76bf3
--- /dev/null
+++ b/template/url.go
@@ -0,0 +1,105 @@
+// Copyright 2011 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 template
+
+import (
+ "bytes"
+ "fmt"
+ "strings"
+)
+
+// urlFilter returns its input unless it contains an unsafe protocol in which
+// case it defangs the entire URL.
+func urlFilter(args ...interface{}) string {
+ s, t := stringify(args...)
+ if t == contentTypeURL {
+ return s
+ }
+ if i := strings.IndexRune(s, ':'); i >= 0 && strings.IndexRune(s[:i], '/') < 0 {
+ protocol := strings.ToLower(s[:i])
+ if protocol != "http" && protocol != "https" && protocol != "mailto" {
+ return "#" + filterFailsafe
+ }
+ }
+ return s
+}
+
+// urlEscaper produces an output that can be embedded in a URL query.
+// The output can be embedded in an HTML attribute without further escaping.
+func urlEscaper(args ...interface{}) string {
+ return urlProcessor(false, args...)
+}
+
+// urlEscaper normalizes URL content so it can be embedded in a quote-delimited
+// string or parenthesis delimited url(...).
+// The normalizer does not encode all HTML specials. Specifically, it does not
+// encode '&' so correct embedding in an HTML attribute requires escaping of
+// '&' to '&'.
+func urlNormalizer(args ...interface{}) string {
+ return urlProcessor(true, args...)
+}
+
+// urlProcessor normalizes (when norm is true) or escapes its input to produce
+// a valid hierarchical or opaque URL part.
+func urlProcessor(norm bool, args ...interface{}) string {
+ s, t := stringify(args...)
+ if t == contentTypeURL {
+ norm = true
+ }
+ var b bytes.Buffer
+ written := 0
+ // The byte loop below assumes that all URLs use UTF-8 as the
+ // content-encoding. This is similar to the URI to IRI encoding scheme
+ // defined in section 3.1 of RFC 3987, and behaves the same as the
+ // EcmaScript builtin encodeURIComponent.
+ // It should not cause any misencoding of URLs in pages with
+ // Content-type: text/html;charset=UTF-8.
+ for i, n := 0, len(s); i < n; i++ {
+ c := s[i]
+ switch c {
+ // Single quote and parens are sub-delims in RFC 3986, but we
+ // escape them so the output can be embedded in single
+ // quoted attributes and unquoted CSS url(...) constructs.
+ // Single quotes are reserved in URLs, but are only used in
+ // the obsolete "mark" rule in an appendix in RFC 3986
+ // so can be safely encoded.
+ case '!', '#', '$', '&', '*', '+', ',', '/', ':', ';', '=', '?', '@', '[', ']':
+ if norm {
+ continue
+ }
+ // Unreserved according to RFC 3986 sec 2.3
+ // "For consistency, percent-encoded octets in the ranges of
+ // ALPHA (%41-%5A and %61-%7A), DIGIT (%30-%39), hyphen (%2D),
+ // period (%2E), underscore (%5F), or tilde (%7E) should not be
+ // created by URI producers
+ case '-', '.', '_', '~':
+ continue
+ case '%':
+ // When normalizing do not re-encode valid escapes.
+ if norm && i+2 < len(s) && isHex(s[i+1]) && isHex(s[i+2]) {
+ continue
+ }
+ default:
+ // Unreserved according to RFC 3986 sec 2.3
+ if 'a' <= c && c <= 'z' {
+ continue
+ }
+ if 'A' <= c && c <= 'Z' {
+ continue
+ }
+ if '0' <= c && c <= '9' {
+ continue
+ }
+ }
+ b.WriteString(s[written:i])
+ fmt.Fprintf(&b, "%%%02x", c)
+ written = i + 1
+ }
+ if written == 0 {
+ return s
+ }
+ b.WriteString(s[written:])
+ return b.String()
+}
diff --git a/template/url_test.go b/template/url_test.go
new file mode 100644
index 00000000..5182e9d7
--- /dev/null
+++ b/template/url_test.go
@@ -0,0 +1,112 @@
+// Copyright 2011 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 template
+
+import (
+ "testing"
+)
+
+func TestURLNormalizer(t *testing.T) {
+ tests := []struct {
+ url, want string
+ }{
+ {"", ""},
+ {
+ "http://example.com:80/foo/bar?q=foo%20&bar=x+y#frag",
+ "http://example.com:80/foo/bar?q=foo%20&bar=x+y#frag",
+ },
+ {" ", "%20"},
+ {"%7c", "%7c"},
+ {"%7C", "%7C"},
+ {"%2", "%252"},
+ {"%", "%25"},
+ {"%z", "%25z"},
+ {"/foo|bar/%5c\u1234", "/foo%7cbar/%5c%e1%88%b4"},
+ }
+ for _, test := range tests {
+ if got := urlNormalizer(test.url); test.want != got {
+ t.Errorf("%q: want\n\t%q\nbut got\n\t%q", test.url, test.want, got)
+ }
+ if test.want != urlNormalizer(test.want) {
+ t.Errorf("not idempotent: %q", test.want)
+ }
+ }
+}
+
+func TestURLFilters(t *testing.T) {
+ input := ("\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" +
+ "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
+ ` !"#$%&'()*+,-./` +
+ `0123456789:;<=>?` +
+ `@ABCDEFGHIJKLMNO` +
+ `PQRSTUVWXYZ[\]^_` +
+ "`abcdefghijklmno" +
+ "pqrstuvwxyz{|}~\x7f" +
+ "\u00A0\u0100\u2028\u2029\ufeff\U0001D11E")
+
+ tests := []struct {
+ name string
+ escaper func(...interface{}) string
+ escaped string
+ }{
+ {
+ "urlEscaper",
+ urlEscaper,
+ "%00%01%02%03%04%05%06%07%08%09%0a%0b%0c%0d%0e%0f" +
+ "%10%11%12%13%14%15%16%17%18%19%1a%1b%1c%1d%1e%1f" +
+ "%20%21%22%23%24%25%26%27%28%29%2a%2b%2c-.%2f" +
+ "0123456789%3a%3b%3c%3d%3e%3f" +
+ "%40ABCDEFGHIJKLMNO" +
+ "PQRSTUVWXYZ%5b%5c%5d%5e_" +
+ "%60abcdefghijklmno" +
+ "pqrstuvwxyz%7b%7c%7d~%7f" +
+ "%c2%a0%c4%80%e2%80%a8%e2%80%a9%ef%bb%bf%f0%9d%84%9e",
+ },
+ {
+ "urlNormalizer",
+ urlNormalizer,
+ "%00%01%02%03%04%05%06%07%08%09%0a%0b%0c%0d%0e%0f" +
+ "%10%11%12%13%14%15%16%17%18%19%1a%1b%1c%1d%1e%1f" +
+ "%20!%22#$%25&%27%28%29*+,-./" +
+ "0123456789:;%3c=%3e?" +
+ "@ABCDEFGHIJKLMNO" +
+ "PQRSTUVWXYZ[%5c]%5e_" +
+ "%60abcdefghijklmno" +
+ "pqrstuvwxyz%7b%7c%7d~%7f" +
+ "%c2%a0%c4%80%e2%80%a8%e2%80%a9%ef%bb%bf%f0%9d%84%9e",
+ },
+ }
+
+ for _, test := range tests {
+ if s := test.escaper(input); s != test.escaped {
+ t.Errorf("%s: want\n\t%q\ngot\n\t%q", test.name, test.escaped, s)
+ continue
+ }
+ }
+}
+
+func BenchmarkURLEscaper(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ urlEscaper("http://example.com:80/foo?q=bar%20&baz=x+y#frag")
+ }
+}
+
+func BenchmarkURLEscaperNoSpecials(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ urlEscaper("TheQuickBrownFoxJumpsOverTheLazyDog.")
+ }
+}
+
+func BenchmarkURLNormalizer(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ urlNormalizer("The quick brown fox jumps over the lazy dog.\n")
+ }
+}
+
+func BenchmarkURLNormalizerNoSpecials(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ urlNormalizer("http://example.com:80/foo?q=bar%20&baz=x+y#frag")
+ }
+}
diff --git a/utils.go b/utils.go
index 533888d1..47cf5319 100644
--- a/utils.go
+++ b/utils.go
@@ -71,6 +71,13 @@ func (h H) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return nil
}
+func (h H) TrySet(k string, v interface{}) H {
+ if _, ok := h[k]; !ok {
+ h[k] = v
+ }
+ return h
+}
+
func filterFlags(content string) string {
for i, char := range content {
if char == ' ' || char == ';' {