diff --git a/debug.go b/debug.go index 0836fc56..2a60b625 100644 --- a/debug.go +++ b/debug.go @@ -6,8 +6,9 @@ package gin import ( "bytes" - "html/template" "log" + + "github.com/gin-gonic/gin/template" ) func init() { diff --git a/gin.go b/gin.go index 3834d67e..add6e6ba 100644 --- a/gin.go +++ b/gin.go @@ -5,13 +5,14 @@ package gin import ( - "html/template" + "bytes" "net" "net/http" "os" "sync" "github.com/gin-gonic/gin/render" + "github.com/gin-gonic/gin/template" ) // Framework's version @@ -107,6 +108,22 @@ func New() *Engine { engine.pool.New = func() interface{} { return engine.allocateContext() } + + // @modified by henry, 2016.1.12 + // Supports nested template + render.HTMLTemplate.Funcs(template.FuncMap{ + "template": func(tname string, data ...interface{}) string { + r := engine.HTMLRender.Instance(tname, data).(render.HTML) + buf := bytes.NewBuffer(nil) + if len(r.Name) == 0 { + r.Template.Execute(buf, r.Data) + } else { + r.Template.ExecuteTemplate(buf, r.Name, r.Data) + } + return buf.String() + }, + }) + return engine } @@ -121,21 +138,23 @@ func (engine *Engine) allocateContext() *Context { return &Context{engine: engine} } +// @modified by henry, 2016.1.12 func (engine *Engine) LoadHTMLGlob(pattern string) { + templ := template.Must(template.Must(render.HTMLTemplate.Clone()).ParseGlob(pattern)) if IsDebugging() { - debugPrintLoadTemplate(template.Must(template.ParseGlob(pattern))) + debugPrintLoadTemplate(templ) engine.HTMLRender = render.HTMLDebug{Glob: pattern} } else { - templ := template.Must(template.ParseGlob(pattern)) engine.SetHTMLTemplate(templ) } } +// @modified by henry, 2016.1.12 func (engine *Engine) LoadHTMLFiles(files ...string) { if IsDebugging() { engine.HTMLRender = render.HTMLDebug{Files: files} } else { - templ := template.Must(template.ParseFiles(files...)) + templ := template.Must(template.Must(render.HTMLTemplate.Clone()).ParseFiles(files...)) engine.SetHTMLTemplate(templ) } } @@ -147,6 +166,23 @@ func (engine *Engine) SetHTMLTemplate(templ *template.Template) { engine.HTMLRender = render.HTMLProduction{Template: templ} } +// @modified by henry, 2016.1.12 +// Set 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 }}. +func (engine *Engine) HTMLTemplateDelims(left, right string) *Engine { + render.HTMLTemplate.Delims(left, right) + return engine +} + +// @modified by henry, 2016.1.12 +// Adds the elements of the argument map to the HTML template's function map. +func (engine *Engine) HTMLTemplateFuncs(funcMap template.FuncMap) *Engine { + render.HTMLTemplate.Funcs(funcMap) + return engine +} + // Adds handlers for NoRoute. It return a 404 code by default. func (engine *Engine) NoRoute(handlers ...HandlerFunc) { engine.noRoute = handlers diff --git a/render/html.go b/render/html.go index 01f6bf22..74062988 100644 --- a/render/html.go +++ b/render/html.go @@ -5,10 +5,14 @@ package render import ( - "html/template" "net/http" + + "github.com/gin-gonic/gin/template" ) +// @modified by henry, 2016.1.12 +var HTMLTemplate = template.New("gin") + type ( HTMLRender interface { Instance(string, interface{}) Render @@ -47,12 +51,14 @@ func (r HTMLDebug) Instance(name string, data interface{}) Render { Data: data, } } + +// @modified by henry, 2016.1.12 func (r HTMLDebug) loadTemplate() *template.Template { if len(r.Files) > 0 { - return template.Must(template.ParseFiles(r.Files...)) + return template.Must(template.Must(HTMLTemplate.Clone()).ParseFiles(r.Files...)) } if len(r.Glob) > 0 { - return template.Must(template.ParseGlob(r.Glob)) + return template.Must(template.Must(HTMLTemplate.Clone()).ParseGlob(r.Glob)) } panic("the HTML debug render was created without files or glob pattern") } diff --git a/template/attr.go b/template/attr.go new file mode 100644 index 00000000..d65d3400 --- /dev/null +++ b/template/attr.go @@ -0,0 +1,175 @@ +// 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 ( + "strings" +) + +// attrTypeMap[n] describes the value of the given attribute. +// If an attribute affects (or can mask) the encoding or interpretation of +// other content, or affects the contents, idempotency, or credentials of a +// network message, then the value in this map is contentTypeUnsafe. +// This map is derived from HTML5, specifically +// http://www.w3.org/TR/html5/Overview.html#attributes-1 +// as well as "%URI"-typed attributes from +// http://www.w3.org/TR/html4/index/attributes.html +var attrTypeMap = map[string]contentType{ + "accept": contentTypePlain, + "accept-charset": contentTypeUnsafe, + "action": contentTypeURL, + "alt": contentTypePlain, + "archive": contentTypeURL, + "async": contentTypeUnsafe, + "autocomplete": contentTypePlain, + "autofocus": contentTypePlain, + "autoplay": contentTypePlain, + "background": contentTypeURL, + "border": contentTypePlain, + "checked": contentTypePlain, + "cite": contentTypeURL, + "challenge": contentTypeUnsafe, + "charset": contentTypeUnsafe, + "class": contentTypePlain, + "classid": contentTypeURL, + "codebase": contentTypeURL, + "cols": contentTypePlain, + "colspan": contentTypePlain, + "content": contentTypeUnsafe, + "contenteditable": contentTypePlain, + "contextmenu": contentTypePlain, + "controls": contentTypePlain, + "coords": contentTypePlain, + "crossorigin": contentTypeUnsafe, + "data": contentTypeURL, + "datetime": contentTypePlain, + "default": contentTypePlain, + "defer": contentTypeUnsafe, + "dir": contentTypePlain, + "dirname": contentTypePlain, + "disabled": contentTypePlain, + "draggable": contentTypePlain, + "dropzone": contentTypePlain, + "enctype": contentTypeUnsafe, + "for": contentTypePlain, + "form": contentTypeUnsafe, + "formaction": contentTypeURL, + "formenctype": contentTypeUnsafe, + "formmethod": contentTypeUnsafe, + "formnovalidate": contentTypeUnsafe, + "formtarget": contentTypePlain, + "headers": contentTypePlain, + "height": contentTypePlain, + "hidden": contentTypePlain, + "high": contentTypePlain, + "href": contentTypeURL, + "hreflang": contentTypePlain, + "http-equiv": contentTypeUnsafe, + "icon": contentTypeURL, + "id": contentTypePlain, + "ismap": contentTypePlain, + "keytype": contentTypeUnsafe, + "kind": contentTypePlain, + "label": contentTypePlain, + "lang": contentTypePlain, + "language": contentTypeUnsafe, + "list": contentTypePlain, + "longdesc": contentTypeURL, + "loop": contentTypePlain, + "low": contentTypePlain, + "manifest": contentTypeURL, + "max": contentTypePlain, + "maxlength": contentTypePlain, + "media": contentTypePlain, + "mediagroup": contentTypePlain, + "method": contentTypeUnsafe, + "min": contentTypePlain, + "multiple": contentTypePlain, + "name": contentTypePlain, + "novalidate": contentTypeUnsafe, + // Skip handler names from + // http://www.w3.org/TR/html5/webappapis.html#event-handlers-on-elements,-document-objects,-and-window-objects + // since we have special handling in attrType. + "open": contentTypePlain, + "optimum": contentTypePlain, + "pattern": contentTypeUnsafe, + "placeholder": contentTypePlain, + "poster": contentTypeURL, + "profile": contentTypeURL, + "preload": contentTypePlain, + "pubdate": contentTypePlain, + "radiogroup": contentTypePlain, + "readonly": contentTypePlain, + "rel": contentTypeUnsafe, + "required": contentTypePlain, + "reversed": contentTypePlain, + "rows": contentTypePlain, + "rowspan": contentTypePlain, + "sandbox": contentTypeUnsafe, + "spellcheck": contentTypePlain, + "scope": contentTypePlain, + "scoped": contentTypePlain, + "seamless": contentTypePlain, + "selected": contentTypePlain, + "shape": contentTypePlain, + "size": contentTypePlain, + "sizes": contentTypePlain, + "span": contentTypePlain, + "src": contentTypeURL, + "srcdoc": contentTypeHTML, + "srclang": contentTypePlain, + "start": contentTypePlain, + "step": contentTypePlain, + "style": contentTypeCSS, + "tabindex": contentTypePlain, + "target": contentTypePlain, + "title": contentTypePlain, + "type": contentTypeUnsafe, + "usemap": contentTypeURL, + "value": contentTypeUnsafe, + "width": contentTypePlain, + "wrap": contentTypePlain, + "xmlns": contentTypeURL, +} + +// attrType returns a conservative (upper-bound on authority) guess at the +// type of the named attribute. +func attrType(name string) contentType { + name = strings.ToLower(name) + if strings.HasPrefix(name, "data-") { + // Strip data- so that custom attribute heuristics below are + // widely applied. + // Treat data-action as URL below. + name = name[5:] + } else if colon := strings.IndexRune(name, ':'); colon != -1 { + if name[:colon] == "xmlns" { + return contentTypeURL + } + // Treat svg:href and xlink:href as href below. + name = name[colon+1:] + } + if t, ok := attrTypeMap[name]; ok { + return t + } + // Treat partial event handler names as script. + if strings.HasPrefix(name, "on") { + return contentTypeJS + } + + // Heuristics to prevent "javascript:..." injection in custom + // data attributes and custom attributes like g:tweetUrl. + // http://www.w3.org/TR/html5/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes + // "Custom data attributes are intended to store custom data + // private to the page or application, for which there are no + // more appropriate attributes or elements." + // Developers seem to store URL content in data URLs that start + // or end with "URI" or "URL". + if strings.Contains(name, "src") || + strings.Contains(name, "uri") || + strings.Contains(name, "url") { + return contentTypeURL + } + return contentTypePlain +} diff --git a/template/clone_test.go b/template/clone_test.go new file mode 100644 index 00000000..e11bff2c --- /dev/null +++ b/template/clone_test.go @@ -0,0 +1,188 @@ +// 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" + "errors" + "io/ioutil" + "testing" + "text/template/parse" +) + +func TestAddParseTree(t *testing.T) { + root := Must(New("root").Parse(`{{define "a"}} {{.}} {{template "b"}} {{.}} ">{{end}}`)) + tree, err := parse.Parse("t", `{{define "b"}}0") + if err != nil { + t.Fatal(err) + } + if got, want := b.String(), ` 1>0 `; got != want { + t.Errorf("got %q want %q", got, want) + } +} + +func TestClone(t *testing.T) { + // The {{.}} will be executed with data "*/" in different contexts. + // In the t0 template, it will be in a text context. + // In the t1 template, it will be in a URL context. + // In the t2 template, it will be in a JavaScript context. + // In the t3 template, it will be in a CSS context. + const tmpl = `{{define "a"}}{{template "lhs"}}{{.}}{{template "rhs"}}{{end}}` + b := new(bytes.Buffer) + + // Create an incomplete template t0. + t0 := Must(New("t0").Parse(tmpl)) + + // Clone t0 as t1. + t1 := Must(t0.Clone()) + Must(t1.Parse(`{{define "lhs"}} {{end}}`)) + + // Execute t1. + b.Reset() + if err := t1.ExecuteTemplate(b, "a", "*/"); err != nil { + t.Fatal(err) + } + if got, want := b.String(), ` `; got != want { + t.Errorf("t1: got %q want %q", got, want) + } + + // Clone t0 as t2. + t2 := Must(t0.Clone()) + Must(t2.Parse(`{{define "lhs"}}

{{end}}`)) + + // Execute t2. + b.Reset() + if err := t2.ExecuteTemplate(b, "a", "*/"); err != nil { + t.Fatal(err) + } + if got, want := b.String(), `

`; got != want { + t.Errorf("t2: got %q want %q", got, want) + } + + // Clone t0 as t3, but do not execute t3 yet. + t3 := Must(t0.Clone()) + Must(t3.Parse(`{{define "lhs"}} {{end}}`)) + + // Complete t0. + Must(t0.Parse(`{{define "lhs"}} ( {{end}}`)) + Must(t0.Parse(`{{define "rhs"}} ) {{end}}`)) + + // Clone t0 as t4. Redefining the "lhs" template should fail. + t4 := Must(t0.Clone()) + if _, err := t4.Parse(`{{define "lhs"}} FAIL {{end}}`); err == nil { + t.Error(`redefine "lhs": got nil err want non-nil`) + } + + // Execute t0. + b.Reset() + if err := t0.ExecuteTemplate(b, "a", "*/"); err != nil { + t.Fatal(err) + } + if got, want := b.String(), ` ( <i>*/ ) `; got != want { + t.Errorf("t0: got %q want %q", got, want) + } + + // Clone t0. This should fail, as t0 has already executed. + if _, err := t0.Clone(); err == nil { + t.Error(`t0.Clone(): got nil err want non-nil`) + } + + // Similarly, cloning sub-templates should fail. + if _, err := t0.Lookup("a").Clone(); err == nil { + t.Error(`t0.Lookup("a").Clone(): got nil err want non-nil`) + } + if _, err := t0.Lookup("lhs").Clone(); err == nil { + t.Error(`t0.Lookup("lhs").Clone(): got nil err want non-nil`) + } + + // Execute t3. + b.Reset() + if err := t3.ExecuteTemplate(b, "a", "*/"); err != nil { + t.Fatal(err) + } + if got, want := b.String(), ` `; got != want { + t.Errorf("t3: got %q want %q", got, want) + } +} + +func TestTemplates(t *testing.T) { + names := []string{"t0", "a", "lhs", "rhs"} + // Some template definitions borrowed from TestClone. + const tmpl = ` + {{define "a"}}{{template "lhs"}}{{.}}{{template "rhs"}}{{end}} + {{define "lhs"}} {{end}}` + t0 := Must(New("t0").Parse(tmpl)) + templates := t0.Templates() + if len(templates) != len(names) { + t.Errorf("expected %d templates; got %d", len(names), len(templates)) + } + for _, name := range names { + found := false + for _, tmpl := range templates { + if name == tmpl.text.Name() { + found = true + break + } + } + if !found { + t.Error("could not find template", name) + } + } +} + +// This used to crash; http://golang.org/issue/3281 +func TestCloneCrash(t *testing.T) { + t1 := New("all") + Must(t1.New("t1").Parse(`{{define "foo"}}foo{{end}}`)) + t1.Clone() +} + +// Ensure that this guarantee from the docs is upheld: +// "Further calls to Parse in the copy will add templates +// to the copy but not to the original." +func TestCloneThenParse(t *testing.T) { + t0 := Must(New("t0").Parse(`{{define "a"}}{{template "embedded"}}{{end}}`)) + t1 := Must(t0.Clone()) + Must(t1.Parse(`{{define "embedded"}}t1{{end}}`)) + if len(t0.Templates())+1 != len(t1.Templates()) { + t.Error("adding a template to a clone added it to the original") + } + // double check that the embedded template isn't available in the original + err := t0.ExecuteTemplate(ioutil.Discard, "a", nil) + if err == nil { + t.Error("expected 'no such template' error") + } +} + +// https://code.google.com/p/go/issues/detail?id=5980 +func TestFuncMapWorksAfterClone(t *testing.T) { + funcs := FuncMap{"customFunc": func() (string, error) { + return "", errors.New("issue5980") + }} + + // get the expected error output (no clone) + uncloned := Must(New("").Funcs(funcs).Parse("{{customFunc}}")) + wantErr := uncloned.Execute(ioutil.Discard, nil) + + // toClone must be the same as uncloned. It has to be recreated from scratch, + // since cloning cannot occur after execution. + toClone := Must(New("").Funcs(funcs).Parse("{{customFunc}}")) + cloned := Must(toClone.Clone()) + gotErr := cloned.Execute(ioutil.Discard, nil) + + if wantErr.Error() != gotErr.Error() { + t.Errorf("clone error message mismatch want %q got %q", wantErr, gotErr) + } +} diff --git a/template/content.go b/template/content.go new file mode 100644 index 00000000..3715ed5c --- /dev/null +++ b/template/content.go @@ -0,0 +1,136 @@ +// 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" + "reflect" +) + +// Strings of content from a trusted source. +type ( + // CSS encapsulates known safe content that matches any of: + // 1. The CSS3 stylesheet production, such as `p { color: purple }`. + // 2. The CSS3 rule production, such as `a[href=~"https:"].foo#bar`. + // 3. CSS3 declaration productions, such as `color: red; margin: 2px`. + // 4. The CSS3 value production, such as `rgba(0, 0, 255, 127)`. + // See http://www.w3.org/TR/css3-syntax/#parsing and + // https://web.archive.org/web/20090211114933/http://w3.org/TR/css3-syntax#style + CSS string + + // HTML encapsulates a known safe HTML document fragment. + // It should not be used for HTML from a third-party, or HTML with + // unclosed tags or comments. The outputs of a sound HTML sanitizer + // and a template escaped by this package are fine for use with HTML. + HTML string + + // HTMLAttr encapsulates an HTML attribute from a trusted source, + // for example, ` dir="ltr"`. + HTMLAttr string + + // JS encapsulates a known safe EcmaScript5 Expression, for example, + // `(x + y * z())`. + // Template authors are responsible for ensuring that typed expressions + // do not break the intended precedence and that there is no + // statement/expression ambiguity as when passing an expression like + // "{ foo: bar() }\n['foo']()", which is both a valid Expression and a + // valid Program with a very different meaning. + JS string + + // JSStr encapsulates a sequence of characters meant to be embedded + // between quotes in a JavaScript expression. + // The string must match a series of StringCharacters: + // StringCharacter :: SourceCharacter but not `\` or LineTerminator + // | EscapeSequence + // Note that LineContinuations are not allowed. + // JSStr("foo\\nbar") is fine, but JSStr("foo\\\nbar") is not. + JSStr string + + // URL encapsulates a known safe URL or URL substring (see RFC 3986). + // A URL like `javascript:checkThatFormNotEditedBeforeLeavingPage()` + // from a trusted source should go in the page, but by default dynamic + // `javascript:` URLs are filtered out since they are a frequently + // exploited injection vector. + URL string +) + +type contentType uint8 + +const ( + contentTypePlain contentType = iota + contentTypeCSS + contentTypeHTML + contentTypeHTMLAttr + contentTypeJS + contentTypeJSStr + contentTypeURL + // contentTypeUnsafe is used in attr.go for values that affect how + // embedded content and network messages are formed, vetted, + // or interpreted; or which credentials network messages carry. + contentTypeUnsafe +) + +// indirect returns the value, after dereferencing as many times +// as necessary to reach the base type (or nil). +func indirect(a interface{}) interface{} { + if a == nil { + return nil + } + if t := reflect.TypeOf(a); t.Kind() != reflect.Ptr { + // Avoid creating a reflect.Value if it's not a pointer. + return a + } + v := reflect.ValueOf(a) + for v.Kind() == reflect.Ptr && !v.IsNil() { + v = v.Elem() + } + return v.Interface() +} + +var ( + errorType = reflect.TypeOf((*error)(nil)).Elem() + fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() +) + +// indirectToStringerOrError returns the value, after dereferencing as many times +// as necessary to reach the base type (or nil) or an implementation of fmt.Stringer +// or error, +func indirectToStringerOrError(a interface{}) interface{} { + if a == nil { + return nil + } + v := reflect.ValueOf(a) + for !v.Type().Implements(fmtStringerType) && !v.Type().Implements(errorType) && v.Kind() == reflect.Ptr && !v.IsNil() { + v = v.Elem() + } + return v.Interface() +} + +// stringify converts its arguments to a string and the type of the content. +// All pointers are dereferenced, as in the text/template package. +func stringify(args ...interface{}) (string, contentType) { + if len(args) == 1 { + switch s := indirect(args[0]).(type) { + case string: + return s, contentTypePlain + case CSS: + return string(s), contentTypeCSS + case HTML: + return string(s), contentTypeHTML + case HTMLAttr: + return string(s), contentTypeHTMLAttr + case JS: + return string(s), contentTypeJS + case JSStr: + return string(s), contentTypeJSStr + case URL: + return string(s), contentTypeURL + } + } + for i, arg := range args { + args[i] = indirectToStringerOrError(arg) + } + return fmt.Sprint(args...), contentTypePlain +} diff --git a/template/content_test.go b/template/content_test.go new file mode 100644 index 00000000..5f3ffe2d --- /dev/null +++ b/template/content_test.go @@ -0,0 +1,280 @@ +// 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" + "testing" +) + +func TestTypedContent(t *testing.T) { + data := []interface{}{ + ` "foo%" O'Reilly &bar;`, + CSS(`a[href =~ "//example.com"]#foo`), + HTML(`Hello, World &tc!`), + HTMLAttr(` dir="ltr"`), + JS(`c && alert("Hello, World!");`), + JSStr(`Hello, World & O'Reilly\x21`), + URL(`greeting=H%69&addressee=(World)`), + } + + // For each content sensitive escaper, see how it does on + // each of the typed strings above. + tests := []struct { + // A template containing a single {{.}}. + input string + want []string + }{ + { + ``, + []string{ + `ZgotmplZ`, + // Allowed but not escaped. + `a[href =~ "//example.com"]#foo`, + `ZgotmplZ`, + `ZgotmplZ`, + `ZgotmplZ`, + `ZgotmplZ`, + `ZgotmplZ`, + }, + }, + { + `
`, + []string{ + `ZgotmplZ`, + // Allowed and HTML escaped. + `a[href =~ "//example.com"]#foo`, + `ZgotmplZ`, + `ZgotmplZ`, + `ZgotmplZ`, + `ZgotmplZ`, + `ZgotmplZ`, + }, + }, + { + `{{.}}`, + []string{ + `<b> "foo%" O'Reilly &bar;`, + `a[href =~ "//example.com"]#foo`, + // Not escaped. + `Hello, World &tc!`, + ` dir="ltr"`, + `c && alert("Hello, World!");`, + `Hello, World & O'Reilly\x21`, + `greeting=H%69&addressee=(World)`, + }, + }, + { + ``, + []string{ + `ZgotmplZ`, + `ZgotmplZ`, + `ZgotmplZ`, + // Allowed and HTML escaped. + ` dir="ltr"`, + `ZgotmplZ`, + `ZgotmplZ`, + `ZgotmplZ`, + }, + }, + { + ``, + []string{ + `<b> "foo%" O'Reilly &bar;`, + `a[href =~ "//example.com"]#foo`, + // Tags stripped, spaces escaped, entity not re-escaped. + `Hello, World &tc!`, + ` dir="ltr"`, + `c && alert("Hello, World!");`, + `Hello, World & O'Reilly\x21`, + `greeting=H%69&addressee=(World)`, + }, + }, + { + ``, + []string{ + `<b> "foo%" O'Reilly &bar;`, + `a[href =~ "//example.com"]#foo`, + // Tags stripped, entity not re-escaped. + `Hello, World &tc!`, + ` dir="ltr"`, + `c && alert("Hello, World!");`, + `Hello, World & O'Reilly\x21`, + `greeting=H%69&addressee=(World)`, + }, + }, + { + ``, + []string{ + `<b> "foo%" O'Reilly &bar;`, + `a[href =~ "//example.com"]#foo`, + // Angle brackets escaped to prevent injection of close tags, entity not re-escaped. + `Hello, <b>World</b> &tc!`, + ` dir="ltr"`, + `c && alert("Hello, World!");`, + `Hello, World & O'Reilly\x21`, + `greeting=H%69&addressee=(World)`, + }, + }, + { + ``, + []string{ + `"\u003cb\u003e \"foo%\" O'Reilly \u0026bar;"`, + `"a[href =~ \"//example.com\"]#foo"`, + `"Hello, \u003cb\u003eWorld\u003c/b\u003e \u0026amp;tc!"`, + `" dir=\"ltr\""`, + // Not escaped. + `c && alert("Hello, World!");`, + // Escape sequence not over-escaped. + `"Hello, World & O'Reilly\x21"`, + `"greeting=H%69\u0026addressee=(World)"`, + }, + }, + { + `