Escape entities in Text and Attrib
This commit is contained in:
parent
b3fd1d386b
commit
c00d92595e
@ -1,11 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
## [0.0.2] - 2019-04-28
|
||||
|
||||
### Added
|
||||
- Changelog
|
||||
- `hatmill.Terms` type for representing lists of nodes
|
||||
|
||||
### Changed
|
||||
- Attrib.WriteTo replaces special characters in Value with HTML entities.
|
||||
- Text.WriteTo replaces special characters with HTML entities.
|
||||
|
||||
## [0.0.1] - 2019-04-28
|
||||
|
||||
Initial development release
|
||||
|
5
go.mod
5
go.mod
@ -1,6 +1,3 @@
|
||||
module gitlab.codemonkeysoftware.net/b/hatmill
|
||||
|
||||
require (
|
||||
github.com/golang/mock v1.2.0 // indirect
|
||||
github.com/leanovate/gopter v0.2.4
|
||||
)
|
||||
require github.com/leanovate/gopter v0.2.4
|
||||
|
2
go.sum
2
go.sum
@ -1,4 +1,2 @@
|
||||
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/leanovate/gopter v0.2.4 h1:U4YLBggDFhJdqQsG4Na2zX7joVTky9vHaj/AGEwSuXU=
|
||||
github.com/leanovate/gopter v0.2.4/go.mod h1:gNcbPWNEWRe4lm+bycKqxUYoH5uoVje5SkOJ3uoLer8=
|
||||
|
61
hatmill.go
61
hatmill.go
@ -1,7 +1,11 @@
|
||||
package hatmill
|
||||
|
||||
//go:generate go run internal/codegen/codegen.go -input defs.json -elemfile element/generated.go -elempkg element -attribfile attribute/generated.go -attribpkg attribute
|
||||
import "io"
|
||||
import (
|
||||
"bytes"
|
||||
"html"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Term represents a fragment of HTML markup, and is one of VoidElement, ParentElement, Terms, or Text.
|
||||
type Term interface {
|
||||
@ -32,7 +36,8 @@ type Attrib struct {
|
||||
}
|
||||
|
||||
// WriteTo writes a to w as an HTML attribute in the form key="value", or
|
||||
// simply key if value is empty. It returns the number of bytes written and any
|
||||
// simply key if value is empty. Special characters in value are replaced with
|
||||
// HTML entities. It returns the number of bytes written and any
|
||||
// error encountered.
|
||||
func (a Attrib) WriteTo(w io.Writer) (n int64, err error) {
|
||||
err = writeStringsTo(w, &n, a.Key)
|
||||
@ -40,7 +45,8 @@ func (a Attrib) WriteTo(w io.Writer) (n int64, err error) {
|
||||
return
|
||||
}
|
||||
if a.Value != "" {
|
||||
err = writeStringsTo(w, &n, "='", a.Value, "'")
|
||||
escaped := html.EscapeString(a.Value)
|
||||
err = writeStringsTo(w, &n, "='", escaped, "'")
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -56,8 +62,6 @@ func (VoidElement) isHtml() {}
|
||||
|
||||
// WriteTo writes the HTML markup represented by e to w, returning the number
|
||||
// of bytes written and any error encountered.
|
||||
//
|
||||
// See the warning about sanitization in the (Attrib).WriteTo documentation.
|
||||
func (e VoidElement) WriteTo(w io.Writer) (n int64, err error) {
|
||||
err = writeStringsTo(w, &n, "<", e.TagName)
|
||||
if err != nil {
|
||||
@ -92,8 +96,6 @@ func (e ParentElement) isHtml() {}
|
||||
|
||||
// WriteTo writes the HTML markup represented by e to w, returning the number
|
||||
// of bytes written and any error encountered.
|
||||
//
|
||||
// See the warning about sanitization in the (Attrib).WriteTo documentation.
|
||||
func (e ParentElement) WriteTo(w io.Writer) (n int64, err error) {
|
||||
n, err = e.VoidElement.WriteTo(w)
|
||||
if err != nil {
|
||||
@ -116,11 +118,50 @@ type Text string
|
||||
|
||||
func (Text) isHtml() {}
|
||||
|
||||
var entities = map[byte][]byte{
|
||||
'&': []byte("&"),
|
||||
'<': []byte("<"),
|
||||
'>': []byte(">"),
|
||||
'\'': []byte("'"),
|
||||
'"': []byte("""),
|
||||
}
|
||||
|
||||
type escapedWriter struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (w escapedWriter) Write(p []byte) (n int, err error) {
|
||||
const specialChars = `&<>'"`
|
||||
for len(p) > 0 {
|
||||
var nCurr int
|
||||
idx := bytes.IndexAny(p, specialChars)
|
||||
if idx == -1 {
|
||||
nCurr, err = w.Writer.Write(p)
|
||||
n += nCurr
|
||||
return
|
||||
}
|
||||
|
||||
nCurr, err = w.Writer.Write(p[:idx])
|
||||
n += nCurr
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
entity := entities[p[idx]]
|
||||
nCurr, err = w.Writer.Write(entity)
|
||||
n += nCurr
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
p = p[idx+1:]
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// WriteTo writes the contents of t to w, returning the number of bytes written
|
||||
// and any error encountered. It does not replace special characters with HTML
|
||||
// entities; use html.EscapeString for this purpose.
|
||||
// and any error encountered. It replaces special characters with HTML entities.
|
||||
func (t Text) WriteTo(w io.Writer) (n int64, err error) {
|
||||
err = writeStringsTo(w, &n, string(t))
|
||||
err = writeStringsTo(escapedWriter{Writer: w}, &n, string(t))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
|
||||
"github.com/leanovate/gopter"
|
||||
"github.com/leanovate/gopter/gen"
|
||||
@ -39,27 +40,58 @@ func checkWrite(wt io.WriterTo, expected string) bool {
|
||||
return err == nil && n == int64(buf.Len()) && buf.String() == expected
|
||||
}
|
||||
|
||||
var ASCII = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{Lo: ' ', Hi: unicode.MaxASCII, Stride: 1},
|
||||
},
|
||||
LatinOffset: 1,
|
||||
}
|
||||
|
||||
var dangerousASCII = gen.UnicodeString(ASCII).SuchThat(func(v interface{}) bool {
|
||||
s := v.(string)
|
||||
const specialChars = `&<>'"`
|
||||
for _, c := range specialChars {
|
||||
if strings.ContainsRune(s, rune(c)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
func printableASCII() string {
|
||||
const minPrintable rune = 32
|
||||
const maxPrintable rune = 126
|
||||
var buf bytes.Buffer
|
||||
for r := minPrintable; r <= maxPrintable; r++ {
|
||||
buf.WriteRune(r)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func TestText(t *testing.T) {
|
||||
properties := gopter.NewProperties(nil)
|
||||
|
||||
properties.Property("WriteTo is correct", prop.ForAll(
|
||||
properties.Property("WriteTo escapes special chars and outputs all others unchanged", prop.ForAll(
|
||||
func(s string) bool {
|
||||
return checkWrite(hatmill.Text(s), s)
|
||||
expected := html.EscapeString(s)
|
||||
return checkWrite(hatmill.Text(s), expected)
|
||||
},
|
||||
gen.AnyString(),
|
||||
dangerousASCII,
|
||||
))
|
||||
|
||||
properties.TestingRun(t)
|
||||
}
|
||||
|
||||
var nonEmptyAlphaString = gen.AlphaString().SuchThat(func(v interface{}) bool {
|
||||
func stringNotEmpty(v interface{}) bool {
|
||||
return v.(string) != ""
|
||||
})
|
||||
}
|
||||
|
||||
var attribGen = gen.Struct(reflect.TypeOf(hatmill.Attrib{}), map[string]gopter.Gen{
|
||||
var nonEmptyAlphaString = gen.AlphaString().SuchThat(stringNotEmpty)
|
||||
|
||||
func attribGen(value gopter.Gen) gopter.Gen {
|
||||
return gen.Struct(reflect.TypeOf(hatmill.Attrib{}), map[string]gopter.Gen{
|
||||
"Key": nonEmptyAlphaString,
|
||||
"Value": gen.AlphaString(),
|
||||
"Value": value,
|
||||
})
|
||||
}
|
||||
|
||||
func TestAttrib(t *testing.T) {
|
||||
properties := gopter.NewProperties(nil)
|
||||
@ -79,9 +111,15 @@ func TestAttrib(t *testing.T) {
|
||||
expected := fmt.Sprintf("%s='%s'", attrib.Key, attrib.Value)
|
||||
return checkWrite(attrib, expected)
|
||||
},
|
||||
attribGen.SuchThat(func(attrib interface{}) bool {
|
||||
return attrib.(hatmill.Attrib).Value != ""
|
||||
}),
|
||||
attribGen(nonEmptyAlphaString),
|
||||
))
|
||||
|
||||
properties.Property("WriteTo escapes attribute value", prop.ForAll(
|
||||
func(attrib hatmill.Attrib) bool {
|
||||
expected := fmt.Sprintf("%s='%s'", attrib.Key, html.EscapeString(attrib.Value))
|
||||
return checkWrite(attrib, expected)
|
||||
},
|
||||
attribGen(dangerousASCII.SuchThat(stringNotEmpty)),
|
||||
))
|
||||
|
||||
properties.TestingRun(t)
|
||||
@ -120,7 +158,7 @@ func TestEmptyElement(t *testing.T) {
|
||||
return checkWrite(elem, expected)
|
||||
},
|
||||
gen.AnyString(),
|
||||
gen.SliceOf(attribGen),
|
||||
gen.SliceOf(attribGen(gen.AlphaString())),
|
||||
))
|
||||
|
||||
properties.TestingRun(t)
|
||||
@ -147,7 +185,7 @@ func TestParentElement(t *testing.T) {
|
||||
return checkWrite(elem, expected)
|
||||
},
|
||||
gen.AnyString(),
|
||||
gen.SliceOf(attribGen),
|
||||
gen.SliceOf(attribGen(gen.AlphaString())),
|
||||
))
|
||||
|
||||
properties.Property("WriteTo writes element correctly with children", prop.ForAll(
|
||||
@ -171,7 +209,7 @@ func TestParentElement(t *testing.T) {
|
||||
return checkWrite(elem, expected)
|
||||
},
|
||||
gen.AnyString(),
|
||||
gen.SliceOf(attribGen),
|
||||
gen.SliceOf(attribGen(gen.AlphaString())),
|
||||
gen.SliceOf(htmlTextGen),
|
||||
))
|
||||
|
||||
@ -199,7 +237,7 @@ func Example() {
|
||||
document := he.Html()(
|
||||
he.Body()(
|
||||
he.Img(ha.Src("./photo.jpg"), ha.Contenteditable(true)),
|
||||
hatmill.Text(html.EscapeString(userInput)),
|
||||
hatmill.Text(userInput),
|
||||
he.Div(ha.Disabled(), ha.CustomData("coolness", "awesome"))(),
|
||||
he.Textarea(ha.Rows(25))(),
|
||||
he.Meter(ha.Min(-1.3), ha.Max(5.5E12))(),
|
||||
|
Loading…
Reference in New Issue
Block a user