Repository: microcosm-cc/bluemonday
Pull request #92 (2019-10-14 13:50:16)

AllowElements regex support
## Overview

This closes https://github.com/microcosm-cc/bluemonday/issues/91

It adds 3 new public methods for allowing elements to be allowed using a regex pattern, they are:

`policy.AllowElementsMatching(regexp)`
`policy.AllowAttrs("span").OnElementsMatching(regexp)`
`policy.AllowStyles("color","mystyle").OnElementsMatching(regexp)`

This means *all* elements that match the regular expression provided, will be affected by the policy being built. This allows a common prefix for example to be used for custom elements and have them all be included in the sanitized output, along with any custom attrs or styles as we can do now on explicitly allowed elements using `AllowElements(...)`

## Rules
Some rules have been implemented, would be good to get feedback:

1) If an element has been explicitly added via `AllowElements` those rules override any regex rules, the regex rules are simply ignored
2) Don't add multiple regex patterns that overlap, as order of execution is not guaranteed and another patterns rules could end up being matched instead of your intended. For example, rules added like `AllowElementsMatching(regexp.MustCompile('^my-element-'))` and `AllowElementsMatching(regexp.MustCompile('^my-element-other'))` would not be recommended as both will get matched and we won't know reliably which rule to use.


Patch:
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c3df40e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+ # Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# goland idea folder
+*.idea
\ No newline at end of file
diff --git a/README.md b/README.md
index ac17b4c..cd2766f 100644
--- a/README.md
+++ b/README.md
@@ -169,12 +169,26 @@ To add elements to a policy either add just the elements:
 p.AllowElements("b", "strong")
 ```
 
+Or using a regex:
+
+_Note: if an element is added by name as shown above, any matching regex will be ignored_
+
+It is also recommended to ensure multiple patterns don't overlap as order of execution is not guaranteed and can result in some rules being missed.
+```go
+p.AllowElementsMatching(regex.MustCompile(`^my-element-`))
+```
+
 Or add elements as a virtue of adding an attribute:
 ```go
 // Not the recommended pattern, see the recommendation on using .Matching() below
 p.AllowAttrs("nowrap").OnElements("td", "th")
 ```
 
+Again, this also supports a regex pattern match alternative:
+```go
+p.AllowAttrs("nowrap").OnElementsMatching(regex.MustCompile(`^my-element-`))
+```
+
 Attributes can either be added to all elements:
 ```go
 p.AllowAttrs("dir").Matching(regexp.MustCompile("(?i)rtl|ltr")).Globally()
@@ -226,6 +240,15 @@ p.AllowAttrs("style").OnElements("span", "p")
 // on 'span' elements only
 p.AllowStyles("text-decoration").MatchingEnum("underline", "line-through", "none").OnElements("span")
 ```
+
+Or you can specify elements based on a regex patterm match:
+```go
+p.AllowAttrs("style").OnElementsMatching(regex.MustCompile(`^my-element-`))
+// Allow the 'text-decoration' property to be set to 'underline', 'line-through' or 'none'
+// on 'span' elements only
+p.AllowStyles("text-decoration").MatchingEnum("underline", "line-through", "none").OnElementsMatching(regex.MustCompile(`^my-element-`))
+```
+
 If you need more specific checking, you can create a handler that takes in a string and returns a bool to
 validate the values for a given property. The string parameter has been
 converted to lowercase and unicode code points have been converted.
diff --git a/go.mod b/go.mod
index 69940a1..47b521a 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/microcosm-cc/bluemonday
 go 1.9
 
 require (
-	github.com/aymerick/douceur v0.2.0
+	github.com/aymerick/douceur v0.2.0 // indirect
 	github.com/chris-ramon/douceur v0.2.0
 	github.com/gorilla/css v1.0.0 // indirect
 	golang.org/x/net v0.0.0-20181220203305-927f97764cc3
diff --git a/policy.go b/policy.go
index 17d3974..739d302 100644
--- a/policy.go
+++ b/policy.go
@@ -86,12 +86,18 @@ type Policy struct {
 	// map[htmlElementName]map[htmlAttributeName]attrPolicy
 	elsAndAttrs map[string]map[string]attrPolicy
 
+	// elsMatchingAndAttrs stores regex based element matches along with attributes
+	elsMatchingAndAttrs map[*regexp.Regexp]map[string]attrPolicy
+
 	// map[htmlAttributeName]attrPolicy
 	globalAttrs map[string]attrPolicy
 
 	// map[htmlElementName]map[cssPropertyName]stylePolicy
 	elsAndStyles map[string]map[string]stylePolicy
 
+	// map[regex]map[cssPropertyName]stylePolicy
+	elsMatchingAndStyles map[*regexp.Regexp]map[string]stylePolicy
+
 	// map[cssPropertyName]stylePolicy
 	globalStyles map[string]stylePolicy
 
@@ -109,6 +115,16 @@ type Policy struct {
 	// be maintained in the output HTML.
 	setOfElementsAllowedWithoutAttrs map[string]struct{}
 
+	// If an element has had all attributes removed as a result of a policy
+	// being applied, then the element would be removed from the output.
+	//
+	// However some elements are valid and have strong layout meaning without
+	// any attributes, i.e. <table>.
+	//
+	// In this case, any element matching a regular expression will be accepted without
+	// attributes added.
+	setOfElementsMatchingAllowedWithoutAttrs []*regexp.Regexp
+
 	setOfElementsToSkipContent map[string]struct{}
 }
 
@@ -156,8 +172,10 @@ type urlPolicy func(url *url.URL) (allowUrl bool)
 func (p *Policy) init() {
 	if !p.initialized {
 		p.elsAndAttrs = make(map[string]map[string]attrPolicy)
+		p.elsMatchingAndAttrs = make(map[*regexp.Regexp]map[string]attrPolicy)
 		p.globalAttrs = make(map[string]attrPolicy)
 		p.elsAndStyles = make(map[string]map[string]stylePolicy)
+		p.elsMatchingAndStyles = make(map[*regexp.Regexp]map[string]stylePolicy)
 		p.globalStyles = make(map[string]stylePolicy)
 		p.allowURLSchemes = make(map[string]urlPolicy)
 		p.setOfElementsAllowedWithoutAttrs = make(map[string]struct{})
@@ -286,6 +304,30 @@ func (abp *attrPolicyBuilder) OnElements(elements ...string) *Policy {
 	return abp.p
 }
 
+// OnElementsMatching will bind an attribute policy to all elements matching a given regex
+// and return the updated policy
+func (abp *attrPolicyBuilder) OnElementsMatching(regex *regexp.Regexp) *Policy {
+	for _, attr := range abp.attrNames {
+		if _, ok := abp.p.elsMatchingAndAttrs[regex]; !ok {
+			abp.p.elsMatchingAndAttrs[regex] = make(map[string]attrPolicy)
+		}
+		ap := attrPolicy{}
+		if abp.regexp != nil {
+			ap.regexp = abp.regexp
+		}
+		abp.p.elsMatchingAndAttrs[regex][attr] = ap
+	}
+
+	if abp.allowEmpty {
+		abp.p.setOfElementsMatchingAllowedWithoutAttrs = append(abp.p.setOfElementsMatchingAllowedWithoutAttrs, regex)
+		if _, ok := abp.p.elsMatchingAndAttrs[regex]; !ok {
+			abp.p.elsMatchingAndAttrs[regex] = make(map[string]attrPolicy)
+		}
+	}
+
+	return abp.p
+}
+
 // Globally will bind an attribute policy to all HTML elements and return the
 // updated policy
 func (abp *attrPolicyBuilder) Globally() *Policy {
@@ -387,6 +429,32 @@ func (spb *stylePolicyBuilder) OnElements(elements ...string) *Policy {
 	return spb.p
 }
 
+// OnElementsMatching will bind a style policy to any HTML elements matching the pattern
+// and return the updated policy
+func (spb *stylePolicyBuilder) OnElementsMatching(regex *regexp.Regexp) *Policy {
+
+		for _, attr := range spb.propertyNames {
+
+			if _, ok := spb.p.elsMatchingAndStyles[regex]; !ok {
+				spb.p.elsMatchingAndStyles[regex] = make(map[string]stylePolicy)
+			}
+
+			sp := stylePolicy{}
+			if spb.handler != nil {
+				sp.handler = spb.handler
+			} else if len(spb.enum) > 0 {
+				sp.enum = spb.enum
+			} else if spb.regexp != nil {
+				sp.regexp = spb.regexp
+			} else {
+				sp.handler = getDefaultHandler(attr)
+			}
+			spb.p.elsMatchingAndStyles[regex][attr] = sp
+		}
+
+	return spb.p
+}
+
 // Globally will bind a style policy to all HTML elements and return the
 // updated policy
 func (spb *stylePolicyBuilder) Globally() *Policy {
@@ -430,6 +498,14 @@ func (p *Policy) AllowElements(names ...string) *Policy {
 	return p
 }
 
+func (p *Policy) AllowElementsMatching(regex *regexp.Regexp) *Policy {
+	p.init()
+	if _, ok := p.elsMatchingAndAttrs[regex]; !ok {
+		p.elsMatchingAndAttrs[regex] = make(map[string]attrPolicy)
+	}
+	return p
+}
+
 // RequireNoFollowOnLinks will result in all a, area, link tags having a
 // rel="nofollow"added to them if one does not already exist
 //
diff --git a/sanitize.go b/sanitize.go
index 0715499..771fc2c 100644
--- a/sanitize.go
+++ b/sanitize.go
@@ -233,14 +233,18 @@ func (p *Policy) sanitize(r io.Reader) *bytes.Buffer {
 
 			aps, ok := p.elsAndAttrs[token.Data]
 			if !ok {
-				if _, ok := p.setOfElementsToSkipContent[token.Data]; ok {
-					skipElementContent = true
-					skippingElementsCount++
-				}
-				if p.addSpaces {
-					buff.WriteString(" ")
+				aa, matched := p.matchRegex(token.Data)
+				if !matched {
+					if _, ok := p.setOfElementsToSkipContent[token.Data]; ok {
+						skipElementContent = true
+						skippingElementsCount++
+					}
+					if p.addSpaces {
+						buff.WriteString(" ")
+					}
+					break
 				}
-				break
+				aps = aa
 			}
 			if len(token.Attr) != 0 {
 				token.Attr = p.sanitizeAttrs(token.Data, token.Attr, aps)
@@ -282,18 +286,27 @@ func (p *Policy) sanitize(r io.Reader) *bytes.Buffer {
 				}
 				break
 			}
-
 			if _, ok := p.elsAndAttrs[token.Data]; !ok {
-				if _, ok := p.setOfElementsToSkipContent[token.Data]; ok {
+				match := false
+				for regex := range p.elsMatchingAndAttrs {
+					if regex.MatchString(token.Data) {
+						skipElementContent = false
+						match = true
+						break
+					}
+				}
+				if _, ok := p.setOfElementsToSkipContent[token.Data]; ok && !match {
 					skippingElementsCount--
 					if skippingElementsCount == 0 {
 						skipElementContent = false
 					}
 				}
-				if p.addSpaces {
-					buff.WriteString(" ")
+				if !match {
+					if p.addSpaces {
+						buff.WriteString(" ")
+					}
+					break
 				}
-				break
 			}
 
 			if !skipElementContent {
@@ -304,10 +317,14 @@ func (p *Policy) sanitize(r io.Reader) *bytes.Buffer {
 
 			aps, ok := p.elsAndAttrs[token.Data]
 			if !ok {
-				if p.addSpaces {
-					buff.WriteString(" ")
+				aa, matched := p.matchRegex(token.Data)
+				if !matched {
+					if p.addSpaces && !matched {
+						buff.WriteString(" ")
+					}
+					break
 				}
-				break
+				aps = aa
 			}
 
 			if len(token.Attr) != 0 {
@@ -317,10 +334,9 @@ func (p *Policy) sanitize(r io.Reader) *bytes.Buffer {
 			if len(token.Attr) == 0 && !p.allowNoAttrs(token.Data) {
 				if p.addSpaces {
 					buff.WriteString(" ")
+					break
 				}
-				break
 			}
-
 			if !skipElementContent {
 				// do not escape multiple query parameters
 				if linkable(token.Data) {
@@ -373,6 +389,17 @@ func (p *Policy) sanitizeAttrs(
 	if len(p.globalStyles) > 0 || (elementHasStylePolicies && len(sps) > 0) {
 		hasStylePolicies = true
 	}
+	// no specific element policy found, look for a pattern match
+	if !hasStylePolicies{
+		for k, v := range p.elsMatchingAndStyles{
+			if k.MatchString(elementName) {
+				if len(v) > 0{
+					hasStylePolicies = true
+					break
+				}
+			}
+		}
+	}
 
 	// Builds a new attribute slice based on the whether the attribute has been
 	// whitelisted explicitly or globally.
@@ -642,6 +669,19 @@ func (p *Policy) sanitizeAttrs(
 
 func (p *Policy) sanitizeStyles(attr html.Attribute, elementName string) html.Attribute {
 	sps := p.elsAndStyles[elementName]
+	if len(sps) == 0{
+		sps = map[string]stylePolicy{}
+		// check for any matching elements, if we don't already have a policy found
+		// if multiple matches are found they will be overwritten, it's best
+		// to not have overlapping matchers
+		for regex, policies :=range p.elsMatchingAndStyles{
+			if regex.MatchString(elementName){
+				for k, v := range policies{
+					sps[k] = v
+				}
+			}
+		}
+	}
 
 	//Add semi-colon to end to fix parsing issue
 	if len(attr.Val) > 0 && attr.Val[len(attr.Val)-1] != ';' {
@@ -652,7 +692,6 @@ func (p *Policy) sanitizeStyles(attr html.Attribute, elementName string) html.At
 		attr.Val = ""
 		return attr
 	}
-
 	clean := []string{}
 	prefixes := []string{"-webkit-", "-moz-", "-ms-", "-o-", "mso-", "-xv-", "-atsc-", "-wap-", "-khtml-", "prince-", "-ah-", "-hp-", "-ro-", "-rim-", "-tc-"}
 
@@ -682,7 +721,6 @@ func (p *Policy) sanitizeStyles(attr html.Attribute, elementName string) html.At
 				continue
 			}
 		}
-
 		if sp, ok := p.globalStyles[tempProperty]; ok && !addedProperty {
 			if sp.handler != nil {
 				if sp.handler(tempValue) {
@@ -710,6 +748,14 @@ func (p *Policy) sanitizeStyles(attr html.Attribute, elementName string) html.At
 
 func (p *Policy) allowNoAttrs(elementName string) bool {
 	_, ok := p.setOfElementsAllowedWithoutAttrs[elementName]
+	if !ok {
+		for _, r := range p.setOfElementsMatchingAllowedWithoutAttrs {
+			if r.MatchString(elementName) {
+				ok = true
+				break
+			}
+		}
+	}
 	return ok
 }
 
@@ -827,3 +873,17 @@ func removeUnicode(value string) string {
 	}
 	return substitutedValue
 }
+
+func (p *Policy) matchRegex(elementName string ) (map[string]attrPolicy, bool) {
+	aps := make(map[string]attrPolicy, 0)
+	matched := false
+	for regex, attrs := range p.elsMatchingAndAttrs {
+		if regex.MatchString(elementName) {
+			matched = true
+			for k, v := range attrs {
+				aps[k] = v
+			}
+		}
+	}
+	return aps, matched
+}


Test patch:
diff --git a/policy_test.go b/policy_test.go
index ee9e2be..b352c30 100644
--- a/policy_test.go
+++ b/policy_test.go
@@ -29,7 +29,10 @@
 
 package bluemonday
 
-import "testing"
+import (
+	"regexp"
+	"testing"
+)
 
 func TestAllowElementsContent(t *testing.T) {
 	policy := NewPolicy().AllowElementsContent("iframe", "script")
@@ -58,3 +61,239 @@ func TestAllowElementsContent(t *testing.T) {
 		}
 	}
 }
+
+func TestAllowElementsMatching(t *testing.T) {
+	tests := map[string]struct {
+		policyFn func(policy *Policy)
+		in       string
+		expected string
+	}{
+		"Self closing tags with regex prefix should strip any that do not match": {
+			policyFn: func(policy *Policy) {
+				policy.AllowElementsMatching(regexp.MustCompile(`^my-element-`))
+			},
+			in:       `<div>
+							<my-element-demo-one data-test="test" my-attr="test"/>
+							<my-element-demo-two data-test="test"/>
+							<not-my-element-demo-one data-test="test"/>
+						</div>`,
+			expected: `<div>
+							<my-element-demo-one data-test="test"/>
+							<my-element-demo-two data-test="test"/>
+							
+						</div>`,
+		}, "Standard elements regex prefix should strip any that do not match": {
+			policyFn: func(policy *Policy) {
+				policy.AllowElementsMatching(regexp.MustCompile(`^my-element-`))
+			},
+			in:       `<div>
+							<my-element-demo-one data-test="test"></my-element-demo-one>
+							<my-element-demo-two data-test="test"></my-element-demo-two>
+							<not-my-element-demo-one data-test="test"></not-my-element-demo-one>
+						</div>`,
+			expected: `<div>
+							<my-element-demo-one data-test="test"></my-element-demo-one>
+							<my-element-demo-two data-test="test"></my-element-demo-two>
+							
+						</div>`,
+		},"Self closing tags with regex prefix and custom attr should strip any that do not match": {
+			policyFn: func(policy *Policy) {
+				policy.AllowElementsMatching(regexp.MustCompile(`^my-element-`))
+				policy.AllowElements("not-my-element-demo-one")
+			},
+			in:       `<div>
+							<my-element-demo-one data-test="test" my-attr="test"/>
+							<my-element-demo-two data-test="test"/>
+							<not-my-element-demo-one data-test="test"/>
+						</div>`,
+			expected: `<div>
+							<my-element-demo-one data-test="test"/>
+							<my-element-demo-two data-test="test"/>
+							<not-my-element-demo-one data-test="test"/>
+						</div>`,
+		},
+	}
+
+	for name, test := range tests {
+		policy := NewPolicy().AllowElements("div")
+		policy.AllowDataAttributes()
+		if test.policyFn != nil{
+			test.policyFn(policy)
+		}
+		out := policy.Sanitize(test.in)
+		if out != test.expected {
+			t.Errorf(
+				"test %s failed;\ninput   : %s\noutput  : %s\nexpected: %s",
+				name,
+				test.in,
+				out,
+				test.expected,
+			)
+		}
+	}
+}
+
+func TestAttrOnElementMatching(t *testing.T){
+	tests := map[string]struct {
+		policyFn func(policy *Policy)
+		in       string
+		expected string
+	}{
+		"Self closing tags with regex prefix should strip any that do not match with custom attr": {
+			policyFn: func(policy *Policy) {
+				policy.AllowAttrs("my-attr").OnElementsMatching(regexp.MustCompile(`^my-element-`))
+			},
+			in:       `<div>
+							<my-element-demo-one data-test="test" my-attr="test"/>
+							<my-element-demo-two data-test="test" other-attr="test"/>
+							<not-my-element-demo-one data-test="test"/>
+						</div>`,
+			expected: `<div>
+							<my-element-demo-one data-test="test" my-attr="test"/>
+							<my-element-demo-two data-test="test"/>
+							
+						</div>`,
+		}, "Standard elements regex prefix should strip any that do not match": {
+			policyFn: func(policy *Policy) {
+				policy.AllowAttrs("my-attr").OnElementsMatching(regexp.MustCompile(`^my-element-`))
+			},
+			in:       `<div>
+							<my-element-demo-one data-test="test" my-attr="test" other-attr="test"></my-element-demo-one>
+							<my-element-demo-two data-test="test" other-attr="test"></my-element-demo-two>
+							<not-my-element-demo-one data-test="test" other-attr="test"></not-my-element-demo-one>
+						</div>`,
+			expected: `<div>
+							<my-element-demo-one data-test="test" my-attr="test"></my-element-demo-one>
+							<my-element-demo-two data-test="test"></my-element-demo-two>
+							
+						</div>`,
+		},"Specific element rule defined should override matching rules": {
+			policyFn: func(policy *Policy) {
+				// specific element rule
+				policy.AllowAttrs("my-other-attr").OnElements("my-element-demo-one")
+				// matched rule takes lower precedence
+				policy.AllowAttrs("my-attr").OnElementsMatching(regexp.MustCompile(`^my-element-`))
+			},
+			in:       `<div>
+							<my-element-demo-one data-test="test" my-attr="test" my-other-attr="test"/>
+							<my-element-demo-two data-test="test" my-attr="test" my-other-attr="test"/>
+							<not-my-element-demo-one data-test="test"/>
+						</div>`,
+			expected: `<div>
+							<my-element-demo-one data-test="test" my-other-attr="test"/>
+							<my-element-demo-two data-test="test" my-attr="test"/>
+							
+						</div>`,
+		},
+	}
+
+	for name, test := range tests {
+		policy := NewPolicy().AllowElements("div")
+		policy.AllowDataAttributes()
+		if test.policyFn != nil{
+			test.policyFn(policy)
+		}
+		out := policy.Sanitize(test.in)
+		if out != test.expected {
+			t.Errorf(
+				"test %s failed;\ninput   : %s\noutput  : %s\nexpected: %s",
+				name,
+				test.in,
+				out,
+				test.expected,
+			)
+		}
+	}
+}
+
+func TestStyleOnElementMatching(t *testing.T){
+	tests := map[string]struct {
+		policyFn func(policy *Policy)
+		in       string
+		expected string
+	}{
+		"Self closing tags with style policy matching prefix should strip any that do not match with custom attr": {
+			policyFn: func(policy *Policy) {
+				policy.AllowAttrs("style").
+					OnElementsMatching(regexp.MustCompile(`^my-element-`))
+				policy.AllowStyles("color","mystyle").
+					MatchingHandler(func(s string) bool {
+					return true
+				}).OnElementsMatching(regexp.MustCompile(`^my-element-`))
+			},
+			in:       `<div>
+							<my-element-demo-one data-test="test" style="color:#ffffff;mystyle:test;other:value"/>
+							<my-element-demo-two data-test="test" other-attr="test" style="other:value"/>
+							<not-my-element-demo-one data-test="test"/>
+						</div>`,
+			expected: `<div>
+							<my-element-demo-one data-test="test" style="color: #ffffff; mystyle: test"/>
+							<my-element-demo-two data-test="test"/>
+							
+						</div>`,
+		}, "Standard elements with style policy and matching elements should strip any styles not allowed": {
+			policyFn: func(policy *Policy) {
+				policy.AllowAttrs("style").
+					OnElementsMatching(regexp.MustCompile(`^my-element-`))
+				policy.AllowStyles("color","mystyle").
+					MatchingHandler(func(s string) bool {
+						return true
+					}).OnElementsMatching(regexp.MustCompile(`^my-element-`))
+			},
+			in:       `<div>
+							<my-element-demo-one data-test="test" style="color:#ffffff;mystyle:test;other:value"></my-element-demo-one>
+							<my-element-demo-two data-test="test" other-attr="test" style="other:value"></my-element-demo-two>
+							<not-my-element-demo-one data-test="test" other-attr="test"></not-my-element-demo-one>
+						</div>`,
+			expected: `<div>
+							<my-element-demo-one data-test="test" style="color: #ffffff; mystyle: test"></my-element-demo-one>
+							<my-element-demo-two data-test="test"></my-element-demo-two>
+							
+						</div>`,
+		},"Specific element rule defined should override matching rules": {
+			policyFn: func(policy *Policy) {
+				policy.AllowAttrs("style").
+					OnElements("my-element-demo-one")
+				policy.AllowStyles("color","mystyle").
+					MatchingHandler(func(s string) bool {
+						return true
+					}).OnElements("my-element-demo-one")
+
+				policy.AllowAttrs("style").
+					OnElementsMatching(regexp.MustCompile(`^my-element-`))
+				policy.AllowStyles("color","customstyle").
+					MatchingHandler(func(s string) bool {
+						return true
+					}).OnElementsMatching(regexp.MustCompile(`^my-element-`))
+			},
+			in:       `<div>
+							<my-element-demo-one data-test="test" style="color:#ffffff;mystyle:test;other:value"/>
+							<my-element-demo-two data-test="test" style="color:#ffffff;mystyle:test;customstyle:value"/>
+							<not-my-element-demo-one data-test="test"/>
+						</div>`,
+			expected: `<div>
+							<my-element-demo-one data-test="test" style="color: #ffffff; mystyle: test"/>
+							<my-element-demo-two data-test="test" style="color: #ffffff; customstyle: value"/>
+							
+						</div>`,
+		},
+	}
+
+	for name, test := range tests {
+		policy := NewPolicy().AllowElements("div")
+		policy.AllowDataAttributes()
+		if test.policyFn != nil{
+			test.policyFn(policy)
+		}
+		out := policy.Sanitize(test.in)
+		if out != test.expected {
+			t.Errorf(
+				"test %s failed;\ninput   : %s\noutput  : %s\nexpected: %s",
+				name,
+				test.in,
+				out,
+				test.expected,
+			)
+		}
+	}
+}
\ No newline at end of file