Escape entities in Text and Attrib
This commit is contained in:
parent
b3fd1d386b
commit
c00d92595e
@ -1,11 +1,15 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [Unreleased]
|
## [0.0.2] - 2019-04-28
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Changelog
|
- Changelog
|
||||||
- `hatmill.Terms` type for representing lists of nodes
|
- `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
|
## [0.0.1] - 2019-04-28
|
||||||
|
|
||||||
Initial development release
|
Initial development release
|
||||||
|
5
go.mod
5
go.mod
@ -1,6 +1,3 @@
|
|||||||
module gitlab.codemonkeysoftware.net/b/hatmill
|
module gitlab.codemonkeysoftware.net/b/hatmill
|
||||||
|
|
||||||
require (
|
require github.com/leanovate/gopter v0.2.4
|
||||||
github.com/golang/mock v1.2.0 // indirect
|
|
||||||
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 h1:U4YLBggDFhJdqQsG4Na2zX7joVTky9vHaj/AGEwSuXU=
|
||||||
github.com/leanovate/gopter v0.2.4/go.mod h1:gNcbPWNEWRe4lm+bycKqxUYoH5uoVje5SkOJ3uoLer8=
|
github.com/leanovate/gopter v0.2.4/go.mod h1:gNcbPWNEWRe4lm+bycKqxUYoH5uoVje5SkOJ3uoLer8=
|
||||||
|
61
hatmill.go
61
hatmill.go
@ -1,7 +1,11 @@
|
|||||||
package hatmill
|
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
|
//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.
|
// Term represents a fragment of HTML markup, and is one of VoidElement, ParentElement, Terms, or Text.
|
||||||
type Term interface {
|
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
|
// 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.
|
// error encountered.
|
||||||
func (a Attrib) WriteTo(w io.Writer) (n int64, err error) {
|
func (a Attrib) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
err = writeStringsTo(w, &n, a.Key)
|
err = writeStringsTo(w, &n, a.Key)
|
||||||
@ -40,7 +45,8 @@ func (a Attrib) WriteTo(w io.Writer) (n int64, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if a.Value != "" {
|
if a.Value != "" {
|
||||||
err = writeStringsTo(w, &n, "='", a.Value, "'")
|
escaped := html.EscapeString(a.Value)
|
||||||
|
err = writeStringsTo(w, &n, "='", escaped, "'")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -56,8 +62,6 @@ func (VoidElement) isHtml() {}
|
|||||||
|
|
||||||
// WriteTo writes the HTML markup represented by e to w, returning the number
|
// WriteTo writes the HTML markup represented by e to w, returning the number
|
||||||
// of bytes written and any error encountered.
|
// 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) {
|
func (e VoidElement) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
err = writeStringsTo(w, &n, "<", e.TagName)
|
err = writeStringsTo(w, &n, "<", e.TagName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -92,8 +96,6 @@ func (e ParentElement) isHtml() {}
|
|||||||
|
|
||||||
// WriteTo writes the HTML markup represented by e to w, returning the number
|
// WriteTo writes the HTML markup represented by e to w, returning the number
|
||||||
// of bytes written and any error encountered.
|
// 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) {
|
func (e ParentElement) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
n, err = e.VoidElement.WriteTo(w)
|
n, err = e.VoidElement.WriteTo(w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -116,11 +118,50 @@ type Text string
|
|||||||
|
|
||||||
func (Text) isHtml() {}
|
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
|
// 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
|
// and any error encountered. It replaces special characters with HTML entities.
|
||||||
// entities; use html.EscapeString for this purpose.
|
|
||||||
func (t Text) WriteTo(w io.Writer) (n int64, err error) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"github.com/leanovate/gopter"
|
"github.com/leanovate/gopter"
|
||||||
"github.com/leanovate/gopter/gen"
|
"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
|
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) {
|
func TestText(t *testing.T) {
|
||||||
properties := gopter.NewProperties(nil)
|
properties := gopter.NewProperties(nil)
|
||||||
|
properties.Property("WriteTo escapes special chars and outputs all others unchanged", prop.ForAll(
|
||||||
properties.Property("WriteTo is correct", prop.ForAll(
|
|
||||||
func(s string) bool {
|
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)
|
properties.TestingRun(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
var nonEmptyAlphaString = gen.AlphaString().SuchThat(func(v interface{}) bool {
|
func stringNotEmpty(v interface{}) bool {
|
||||||
return v.(string) != ""
|
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,
|
"Key": nonEmptyAlphaString,
|
||||||
"Value": gen.AlphaString(),
|
"Value": value,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestAttrib(t *testing.T) {
|
func TestAttrib(t *testing.T) {
|
||||||
properties := gopter.NewProperties(nil)
|
properties := gopter.NewProperties(nil)
|
||||||
@ -79,9 +111,15 @@ func TestAttrib(t *testing.T) {
|
|||||||
expected := fmt.Sprintf("%s='%s'", attrib.Key, attrib.Value)
|
expected := fmt.Sprintf("%s='%s'", attrib.Key, attrib.Value)
|
||||||
return checkWrite(attrib, expected)
|
return checkWrite(attrib, expected)
|
||||||
},
|
},
|
||||||
attribGen.SuchThat(func(attrib interface{}) bool {
|
attribGen(nonEmptyAlphaString),
|
||||||
return attrib.(hatmill.Attrib).Value != ""
|
))
|
||||||
}),
|
|
||||||
|
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)
|
properties.TestingRun(t)
|
||||||
@ -120,7 +158,7 @@ func TestEmptyElement(t *testing.T) {
|
|||||||
return checkWrite(elem, expected)
|
return checkWrite(elem, expected)
|
||||||
},
|
},
|
||||||
gen.AnyString(),
|
gen.AnyString(),
|
||||||
gen.SliceOf(attribGen),
|
gen.SliceOf(attribGen(gen.AlphaString())),
|
||||||
))
|
))
|
||||||
|
|
||||||
properties.TestingRun(t)
|
properties.TestingRun(t)
|
||||||
@ -147,7 +185,7 @@ func TestParentElement(t *testing.T) {
|
|||||||
return checkWrite(elem, expected)
|
return checkWrite(elem, expected)
|
||||||
},
|
},
|
||||||
gen.AnyString(),
|
gen.AnyString(),
|
||||||
gen.SliceOf(attribGen),
|
gen.SliceOf(attribGen(gen.AlphaString())),
|
||||||
))
|
))
|
||||||
|
|
||||||
properties.Property("WriteTo writes element correctly with children", prop.ForAll(
|
properties.Property("WriteTo writes element correctly with children", prop.ForAll(
|
||||||
@ -171,7 +209,7 @@ func TestParentElement(t *testing.T) {
|
|||||||
return checkWrite(elem, expected)
|
return checkWrite(elem, expected)
|
||||||
},
|
},
|
||||||
gen.AnyString(),
|
gen.AnyString(),
|
||||||
gen.SliceOf(attribGen),
|
gen.SliceOf(attribGen(gen.AlphaString())),
|
||||||
gen.SliceOf(htmlTextGen),
|
gen.SliceOf(htmlTextGen),
|
||||||
))
|
))
|
||||||
|
|
||||||
@ -199,7 +237,7 @@ func Example() {
|
|||||||
document := he.Html()(
|
document := he.Html()(
|
||||||
he.Body()(
|
he.Body()(
|
||||||
he.Img(ha.Src("./photo.jpg"), ha.Contenteditable(true)),
|
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.Div(ha.Disabled(), ha.CustomData("coolness", "awesome"))(),
|
||||||
he.Textarea(ha.Rows(25))(),
|
he.Textarea(ha.Rows(25))(),
|
||||||
he.Meter(ha.Min(-1.3), ha.Max(5.5E12))(),
|
he.Meter(ha.Min(-1.3), ha.Max(5.5E12))(),
|
||||||
|
Loading…
Reference in New Issue
Block a user