Skip to content

Commit

Permalink
Double-bracket variables, {{var}}, are now HTML-escaping by default, …
Browse files Browse the repository at this point in the history
…and triple-bracket vars, {{{var}}}, are raw
  • Loading branch information
hoisie committed Oct 12, 2010
1 parent 2543947 commit 41bb4a2
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 7 deletions.
4 changes: 4 additions & 0 deletions Readme.md
Expand Up @@ -37,6 +37,10 @@ If you're planning to render the same template multiple times, you do it efficie

For more example usage, please see `mustache_test.go`

## Escaping

mustache.go follows the official mustache HTML escaping rules. That is, if you enclose a variable with two curly brackets, `{{var}}`, the contents are HTML-escaped. For instance, strings like `5 > 2` are converted to `5 > 2`. To use raw characters, use three curly brackets `{{{var}}}`.

## Supported features

* Variables
Expand Down
74 changes: 67 additions & 7 deletions mustache.go
Expand Up @@ -18,6 +18,7 @@ type textElement struct {

type varElement struct {
name string
raw bool
}

type sectionElement struct {
Expand All @@ -44,6 +45,40 @@ type parseError struct {

func (p parseError) String() string { return fmt.Sprintf("line %d: %s", p.line, p.message) }

var (
esc_quot = []byte(""")
esc_apos = []byte("'")
esc_amp = []byte("&")
esc_lt = []byte("<")
esc_gt = []byte(">")
)

// taken from pkg/template
func htmlEscape(w io.Writer, s []byte) {
var esc []byte
last := 0
for i, c := range s {
switch c {
case '"':
esc = esc_quot
case '\'':
esc = esc_apos
case '&':
esc = esc_amp
case '<':
esc = esc_lt
case '>':
esc = esc_gt
default:
continue
}
w.Write(s[last:i])
w.Write(esc)
last = i + 1
}
w.Write(s[last:])
}

func (tmpl *Template) readString(s string) (string, os.Error) {
i := tmpl.p
newlines := 0
Expand Down Expand Up @@ -128,15 +163,20 @@ func (tmpl *Template) parseSection(section *sectionElement) os.Error {
// put text into an item
text = text[0 : len(text)-len(tmpl.otag)]
section.elems.Push(&textElement{[]byte(text)})

text, err = tmpl.readString(tmpl.ctag)
if tmpl.p < len(tmpl.data) && tmpl.data[tmpl.p] == '{' {
text, err = tmpl.readString("}" + tmpl.ctag)
} else {
text, err = tmpl.readString(tmpl.ctag)
}

if err == os.EOF {
//put the remaining text in a block
return parseError{tmpl.curline, "unmatched open tag"}
}

//trim the close tag off the text
tag := strings.TrimSpace(text[0 : len(text)-len(tmpl.ctag)])

if len(tag) == 0 {
return parseError{tmpl.curline, "empty tag"}
}
Expand Down Expand Up @@ -184,8 +224,13 @@ func (tmpl *Template) parseSection(section *sectionElement) os.Error {
tmpl.otag = newtags[0]
tmpl.ctag = newtags[1]
}
case '{':
if tag[len(tag) -1] == '}' {
//use a raw tag
section.elems.Push(&varElement{tag[1:len(tag)-1], true})
}
default:
section.elems.Push(&varElement{tag})
section.elems.Push(&varElement{tag, false})
}
}

Expand All @@ -205,8 +250,13 @@ func (tmpl *Template) parse() os.Error {
// put text into an item
text = text[0 : len(text)-len(tmpl.otag)]
tmpl.elems.Push(&textElement{[]byte(text)})

text, err = tmpl.readString(tmpl.ctag)

if tmpl.p < len(tmpl.data) && tmpl.data[tmpl.p] == '{' {
text, err = tmpl.readString("}" + tmpl.ctag)
} else {
text, err = tmpl.readString(tmpl.ctag)
}

if err == os.EOF {
//put the remaining text in a block
return parseError{tmpl.curline, "unmatched open tag"}
Expand Down Expand Up @@ -255,8 +305,13 @@ func (tmpl *Template) parse() os.Error {
tmpl.otag = newtags[0]
tmpl.ctag = newtags[1]
}
case '{':
//use a raw tag
if tag[len(tag) -1] == '}' {
tmpl.elems.Push(&varElement{tag[1:len(tag)-1], true})
}
default:
tmpl.elems.Push(&varElement{tag})
tmpl.elems.Push(&varElement{tag, false})
}
}

Expand Down Expand Up @@ -429,7 +484,12 @@ func renderElement(element interface{}, contextChain *vector.Vector, buf io.Writ
case *varElement:
val := lookup(contextChain, elem.name)
if val != nil {
fmt.Fprint(buf, val.Interface())
if elem.raw {
fmt.Fprint(buf, val.Interface())
} else {
s := fmt.Sprint(val.Interface())
htmlEscape(buf,[]byte(s))
}
}
case *sectionElement:
renderSection(elem, contextChain, buf)
Expand Down
4 changes: 4 additions & 0 deletions mustache_test.go
Expand Up @@ -74,6 +74,8 @@ func makeVector(n int) *vector.Vector {
var tests = []Test{
Test{`hello world`, nil, "hello world"},
Test{`hello {{name}}`, map[string]string{"name": "world"}, "hello world"},
Test{`{{var}}`, map[string]string { "var": "5 > 2" }, "5 &gt; 2"},
Test{`{{{var}}}`, map[string]string { "var": "5 > 2" }, "5 > 2"},
Test{`{{a}}{{b}}{{c}}{{d}}`, map[string]string{"a": "a", "b": "b", "c": "c", "d": "d"}, "abcd"},
Test{`0{{a}}1{{b}}23{{c}}456{{d}}89`, map[string]string{"a": "a", "b": "b", "c": "c", "d": "d"}, "0a1b23c456d89"},
Test{`hello {{! comment }}world`, map[string]string{}, "hello world"},
Expand All @@ -88,6 +90,8 @@ var tests = []Test{

//section tests
Test{`{{#a}}{{b}}{{/a}}`, Data{true, "hello"}, "hello"},
Test{`{{#a}}{{{b}}}{{/a}}`, Data{true, "5 > 2"}, "5 > 2"},
Test{`{{#a}}{{b}}{{/a}}`, Data{true, "5 > 2"}, "5 &gt; 2"},
Test{`{{#a}}{{b}}{{/a}}`, Data{false, "hello"}, ""},
Test{`{{a}}{{#b}}{{b}}{{/b}}{{c}}`, map[string]string{"a": "a", "b": "b", "c": "c"}, "abc"},
Test{`{{#a}}{{b}}{{/a}}`, struct {
Expand Down

0 comments on commit 41bb4a2

Please sign in to comment.