diff --git a/bpf/doc.go b/bpf/doc.go
index 04ec1c8..1ea566b 100644
--- a/bpf/doc.go
+++ b/bpf/doc.go
@@ -49,6 +49,13 @@
 functions. Currently, the only extensions supported by this package
 are the Linux packet filter extensions.
 
+# Security Considerations
+
+The implementation of the BPF VM in this package is suitable for
+testing BPF programs. It aims for consistency with other BPF VM
+implementations, but divergence in behavior is not considered a
+security issue.
+
 # Examples
 
 This packet filter selects all ARP packets.
diff --git a/dns/dnsmessage/message_test.go b/dns/dnsmessage/message_test.go
index 03f9b42..9e46c63 100644
--- a/dns/dnsmessage/message_test.go
+++ b/dns/dnsmessage/message_test.go
@@ -1924,3 +1924,63 @@
 		t.Fatalf("msg[maxPtr:] = %v, want: %v", msg[maxPtr:], expect)
 	}
 }
+
+func TestInvalidMessages(t *testing.T) {
+	for _, test := range []struct {
+		name string
+		in   []byte
+	}{
+		{
+			name: "invalid SVCB",
+			in: []byte{
+				0x12, 0x34, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+				0x00, 0x00, 0x41, 0x00, 0x01,
+				0x00, 0x00, 0x41, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+				0x00, 0x64,
+				0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x5d,
+			},
+		},
+		{
+			name: "SVCB out-of-order keys",
+			in: []byte{
+				0x12, 0x34, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+				0x00, 0x00, 0x41, 0x00, 0x01,
+				0x00, 0x00, 0x41, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+				0x00, 0x0b,
+				0x00, 0x01, 0x00,
+				0x00, 0x02, 0x00, 0x00,
+				0x00, 0x01, 0x00, 0x00,
+			},
+		},
+		{
+			name: "SVCB duplicate keys",
+			in: []byte{
+				0x12, 0x34, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+				0x00, 0x00, 0x41, 0x00, 0x01,
+				0x00, 0x00, 0x41, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+				0x00, 0x0b,
+				0x00, 0x01, 0x00,
+				0x00, 0x01, 0x00, 0x00,
+				0x00, 0x01, 0x00, 0x00,
+			},
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			var p Parser
+			_, err := p.Start(test.in)
+			if err != nil {
+				return
+			}
+			_, err = p.AllQuestions()
+			if err != nil {
+				return
+			}
+			_, err = p.AllAnswers()
+			if err != nil {
+				return
+			}
+			t.Errorf("successfully parsed message: want error")
+			t.Errorf("message: {%x}", test.in)
+		})
+	}
+}
diff --git a/dns/dnsmessage/svcb.go b/dns/dnsmessage/svcb.go
index 4840516..7729378 100644
--- a/dns/dnsmessage/svcb.go
+++ b/dns/dnsmessage/svcb.go
@@ -168,9 +168,8 @@
 	if err != nil {
 		return oldMsg, &nestedError{"SVCBResource.Target", err}
 	}
-	var previousKey SVCParamKey
 	for i, param := range r.Params {
-		if i > 0 && param.Key <= previousKey {
+		if i > 0 && param.Key <= r.Params[i-1].Key {
 			return oldMsg, &nestedError{"SVCBResource.Params", errParamOutOfOrder}
 		}
 		if len(param.Value) > (1<<16)-1 {
@@ -205,7 +204,7 @@
 	off = paramsOff
 	var previousKey uint16
 	for off < bodyEnd {
-		var key, len uint16
+		var key, size uint16
 		if key, off, err = unpackUint16(msg, off); err != nil {
 			return SVCBResource{}, &nestedError{"Params key", err}
 		}
@@ -214,14 +213,15 @@
 			// consider the RR malformed if the SvcParamKeys are not in strictly increasing numeric order
 			return SVCBResource{}, &nestedError{"Params", errParamOutOfOrder}
 		}
-		if len, off, err = unpackUint16(msg, off); err != nil {
+		if size, off, err = unpackUint16(msg, off); err != nil {
 			return SVCBResource{}, &nestedError{"Params value length", err}
 		}
-		if off+int(len) > bodyEnd {
+		if off+int(size) > bodyEnd {
 			return SVCBResource{}, errResourceLen
 		}
-		totalValueLen += len
-		off += int(len)
+		previousKey = key
+		totalValueLen += size
+		off += int(size)
 		n++
 	}
 	if off != bodyEnd {
@@ -236,20 +236,23 @@
 	off = paramsOff
 	for i := 0; i < n; i++ {
 		p := &r.Params[i]
-		var key, len uint16
+		var key, size uint16
 		if key, off, err = unpackUint16(msg, off); err != nil {
 			return SVCBResource{}, &nestedError{"param key", err}
 		}
 		p.Key = SVCParamKey(key)
-		if len, off, err = unpackUint16(msg, off); err != nil {
+		if size, off, err = unpackUint16(msg, off); err != nil {
 			return SVCBResource{}, &nestedError{"param length", err}
 		}
-		if copy(valuesBuf, msg[off:off+int(len)]) != int(len) {
+		if len(msg[off:]) < int(size) {
 			return SVCBResource{}, &nestedError{"param value", errCalcLen}
 		}
-		p.Value = valuesBuf[:len:len]
-		valuesBuf = valuesBuf[len:]
-		off += int(len)
+		if copy(valuesBuf, msg[off:][:int(size)]) != int(size) {
+			return SVCBResource{}, &nestedError{"param value", errCalcLen}
+		}
+		p.Value = valuesBuf[:size:size]
+		valuesBuf = valuesBuf[size:]
+		off += int(size)
 	}
 
 	return r, nil
diff --git a/dns/dnsmessage/svcb_test.go b/dns/dnsmessage/svcb_test.go
index 74fcccd..64a1bfd 100644
--- a/dns/dnsmessage/svcb_test.go
+++ b/dns/dnsmessage/svcb_test.go
@@ -366,28 +366,74 @@
 	testRecord(bytes, parsed)
 }
 
-func TestSVCBPackLongValue(t *testing.T) {
-	b := NewBuilder(nil, Header{})
-	b.StartQuestions()
-	b.StartAnswers()
-
-	res := SVCBResource{
-		Target: MustNewName("example.com."),
-		Params: []SVCParam{
-			{
-				Key:   SVCParamMandatory,
-				Value: make([]byte, math.MaxUint16+1),
+func TestSVCBPackErrors(t *testing.T) {
+	for _, test := range []struct {
+		name string
+		r    SVCBResource
+	}{
+		{
+			name: "long value",
+			r: SVCBResource{
+				Target: MustNewName("example.com."),
+				Params: []SVCParam{
+					{
+						Key:   SVCParamMandatory,
+						Value: make([]byte, math.MaxUint16+1),
+					},
+				},
 			},
 		},
-	}
+		{
+			name: "out-of-order keys",
+			r: SVCBResource{
+				Target: MustNewName("example.com."),
+				Params: []SVCParam{
+					{
+						Key:   SVCParamPort, // 3
+						Value: []byte("443"),
+					},
+					{
+						Key:   SVCParamALPN, // 1
+						Value: []byte("h2"),
+					},
+				},
+			},
+		},
+		{
+			name: "duplicate keys",
+			r: SVCBResource{
+				Target: MustNewName("example.com."),
+				Params: []SVCParam{
+					{
+						Key:   SVCParamALPN,
+						Value: []byte("h3"),
+					},
+					{
+						Key:   SVCParamALPN,
+						Value: []byte("h2"),
+					},
+				},
+			},
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			b := NewBuilder(nil, Header{})
+			b.StartQuestions()
+			b.StartAnswers()
+			if err := b.SVCBResource(ResourceHeader{
+				Name: MustNewName("example.com."),
+			}, test.r); err == nil {
+				t.Errorf("b.SVCBResource() succeeded; want error")
+			}
 
-	err := b.SVCBResource(ResourceHeader{Name: MustNewName("example.com.")}, res)
-	if err == nil || err.Error() != "ResourceBody: SVCBResource.Params: value too long (>65535 bytes)" {
-		t.Fatalf(`b.SVCBResource() = %v; want = "ResourceBody: SVCBResource.Params: value too long (>65535 bytes)"`, err)
-	}
-
-	err = b.HTTPSResource(ResourceHeader{Name: MustNewName("example.com.")}, HTTPSResource{res})
-	if err == nil || err.Error() != "ResourceBody: SVCBResource.Params: value too long (>65535 bytes)" {
-		t.Fatalf(`b.HTTPSResource() = %v; want = "ResourceBody: SVCBResource.Params: value too long (>65535 bytes)"`, err)
+			b = NewBuilder(nil, Header{})
+			b.StartQuestions()
+			b.StartAnswers()
+			if err := b.HTTPSResource(ResourceHeader{
+				Name: MustNewName("example.com."),
+			}, HTTPSResource{test.r}); err == nil {
+				t.Errorf("b.HTTPSResource() succeeded; want error")
+			}
+		})
 	}
 }
diff --git a/go.mod b/go.mod
index 42e19a7..bb33976 100644
--- a/go.mod
+++ b/go.mod
@@ -3,8 +3,8 @@
 go 1.25.0
 
 require (
-	golang.org/x/crypto v0.52.0
-	golang.org/x/sys v0.45.0
-	golang.org/x/term v0.43.0
-	golang.org/x/text v0.37.0
+	golang.org/x/crypto v0.54.0
+	golang.org/x/sys v0.47.0
+	golang.org/x/term v0.45.0
+	golang.org/x/text v0.40.0
 )
diff --git a/go.sum b/go.sum
index 5a66008..b68816c 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,8 @@
-golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
-golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
-golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
-golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
-golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
-golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
-golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
-golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
+golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
+golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
+golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
+golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
+golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
+golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
+golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
diff --git a/html/foreign.go b/html/foreign.go
index e8515d8..65d01d1 100644
--- a/html/foreign.go
+++ b/html/foreign.go
@@ -23,7 +23,7 @@
 		}
 		switch a.Key {
 		case "xlink:actuate", "xlink:arcrole", "xlink:href", "xlink:role", "xlink:show",
-			"xlink:title", "xlink:type", "xml:base", "xml:lang", "xml:space", "xmlns:xlink":
+			"xlink:title", "xlink:type", "xml:lang", "xml:space", "xmlns:xlink":
 			j := strings.Index(a.Key, ":")
 			aa[i].Namespace = a.Key[:j]
 			aa[i].Key = a.Key[j+1:]
diff --git a/html/parse.go b/html/parse.go
index b3d2a25..165b610 100644
--- a/html/parse.go
+++ b/html/parse.go
@@ -63,7 +63,7 @@
 // Stop tags for use in popUntil. These come from section 12.2.4.2.
 var (
 	defaultScopeStopTags = map[string][]a.Atom{
-		"":     {a.Applet, a.Caption, a.Html, a.Table, a.Td, a.Th, a.Marquee, a.Object, a.Template},
+		"":     {a.Applet, a.Caption, a.Html, a.Table, a.Td, a.Th, a.Marquee, a.Object, a.Template, a.Select},
 		"math": {a.AnnotationXml, a.Mi, a.Mn, a.Mo, a.Ms, a.Mtext},
 		"svg":  {a.Desc, a.ForeignObject, a.Title},
 	}
@@ -78,7 +78,6 @@
 	tableScope
 	tableRowScope
 	tableBodyScope
-	selectScope
 )
 
 // popUntil pops the stack of open elements at the highest element whose tag
@@ -133,10 +132,6 @@
 				if tagAtom == a.Html || tagAtom == a.Table || tagAtom == a.Template {
 					return -1
 				}
-			case selectScope:
-				if tagAtom != a.Optgroup && tagAtom != a.Option {
-					return -1
-				}
 			default:
 				panic(fmt.Sprintf("html: internal error: indexOfElementInScope unknown scope: %d", s))
 			}
@@ -460,21 +455,6 @@
 		}
 
 		switch n.DataAtom {
-		case a.Select:
-			if !last {
-				for ancestor, first := n, p.oe[0]; ancestor != first; {
-					ancestor = p.oe[p.oe.index(ancestor)-1]
-					switch ancestor.DataAtom {
-					case a.Template:
-						p.im = inSelectIM
-						return
-					case a.Table:
-						p.im = inSelectInTableIM
-						return
-					}
-				}
-			}
-			p.im = inSelectIM
 		case a.Td, a.Th:
 			// TODO: remove this divergence from the HTML5 spec.
 			//
@@ -1002,7 +982,10 @@
 			p.popUntil(buttonScope, a.P)
 			p.addElement()
 		case a.Button:
-			p.popUntil(defaultScope, a.Button)
+			if p.elementInScope(defaultScope, a.Button) {
+				p.generateImpliedEndTags()
+				p.popUntil(defaultScope, a.Button)
+			}
 			p.reconstructActiveFormattingElements()
 			p.addElement()
 			p.framesetOK = false
@@ -1040,7 +1023,18 @@
 			p.framesetOK = false
 			p.im = inTableIM
 			return true
-		case a.Area, a.Br, a.Embed, a.Img, a.Input, a.Keygen, a.Wbr:
+		case a.Area, a.Br, a.Embed, a.Img, a.Keygen, a.Wbr:
+			p.reconstructActiveFormattingElements()
+			p.addElement()
+			p.oe.pop()
+			p.acknowledgeSelfClosingTag()
+			p.framesetOK = false
+		case a.Input:
+			if p.fragment && p.context.DataAtom == a.Select {
+				// Ignore the token.
+				return true
+			}
+			p.popUntil(defaultScope, a.Select)
 			p.reconstructActiveFormattingElements()
 			p.addElement()
 			p.oe.pop()
@@ -1061,7 +1055,13 @@
 			p.oe.pop()
 			p.acknowledgeSelfClosingTag()
 		case a.Hr:
-			p.popUntil(buttonScope, a.P)
+			if p.elementInScope(buttonScope, a.P) {
+				p.generateImpliedEndTags("p")
+				p.popUntil(defaultScope, a.P)
+			}
+			if p.elementInScope(defaultScope, a.Select) {
+				p.generateImpliedEndTags()
+			}
 			p.addElement()
 			p.oe.pop()
 			p.acknowledgeSelfClosingTag()
@@ -1095,13 +1095,30 @@
 			// Don't let the tokenizer go into raw text mode when scripting is disabled.
 			p.tokenizer.NextIsNotRawText()
 		case a.Select:
+			if p.fragment && p.context.DataAtom == a.Select {
+				// Ignore the token.
+				return true
+			} else if p.popUntil(defaultScope, a.Select) {
+				return true
+			}
 			p.reconstructActiveFormattingElements()
 			p.addElement()
 			p.framesetOK = false
-			p.im = inSelectIM
 			return true
-		case a.Optgroup, a.Option:
-			if p.top().DataAtom == a.Option {
+		case a.Option:
+			if p.elementInScope(defaultScope, a.Select) {
+				p.generateImpliedEndTags("optgroup")
+				// If oe has option element in scope, parse error?
+			} else if p.top().DataAtom == a.Option {
+				p.oe.pop()
+			}
+			p.reconstructActiveFormattingElements()
+			p.addElement()
+		case a.Optgroup:
+			if p.elementInScope(defaultScope, a.Select) {
+				p.generateImpliedEndTags()
+				// If oe has option or optgroup element in scope, parse error?
+			} else if p.top().DataAtom == a.Option {
 				p.oe.pop()
 			}
 			p.reconstructActiveFormattingElements()
@@ -1149,7 +1166,12 @@
 				return false
 			}
 			return true
-		case a.Address, a.Article, a.Aside, a.Blockquote, a.Button, a.Center, a.Details, a.Dialog, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Listing, a.Main, a.Menu, a.Nav, a.Ol, a.Pre, a.Search, a.Section, a.Summary, a.Ul:
+		case a.Address, a.Article, a.Aside, a.Blockquote, a.Button, a.Center, a.Details, a.Dialog, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Listing, a.Main, a.Menu, a.Nav, a.Ol, a.Pre, a.Search, a.Section, a.Select, a.Summary, a.Ul:
+			if !p.elementInScope(defaultScope, p.tok.DataAtom) {
+				// Ignore the token.
+				return true
+			}
+			p.generateImpliedEndTags()
 			p.popUntil(defaultScope, p.tok.DataAtom)
 		case a.Form:
 			if p.oe.contains(a.Template) {
@@ -1488,17 +1510,6 @@
 			}
 			p.addElement()
 			p.form = p.oe.pop()
-		case a.Select:
-			p.reconstructActiveFormattingElements()
-			switch p.top().DataAtom {
-			case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr:
-				p.fosterParenting = true
-			}
-			p.addElement()
-			p.fosterParenting = false
-			p.framesetOK = false
-			p.im = inSelectInTableIM
-			return true
 		}
 	case EndTagToken:
 		switch p.tok.DataAtom {
@@ -1547,12 +1558,6 @@
 			p.clearActiveFormattingElements()
 			p.im = inTableIM
 			return false
-		case a.Select:
-			p.reconstructActiveFormattingElements()
-			p.addElement()
-			p.framesetOK = false
-			p.im = inSelectInTableIM
-			return true
 		}
 	case EndTagToken:
 		switch p.tok.DataAtom {
@@ -1762,12 +1767,6 @@
 			}
 			// Ignore the token.
 			return true
-		case a.Select:
-			p.reconstructActiveFormattingElements()
-			p.addElement()
-			p.framesetOK = false
-			p.im = inSelectInTableIM
-			return true
 		}
 	case EndTagToken:
 		switch p.tok.DataAtom {
@@ -1798,118 +1797,6 @@
 	return inBodyIM(p)
 }
 
-// Section 12.2.6.4.16.
-func inSelectIM(p *parser) bool {
-	switch p.tok.Type {
-	case TextToken:
-		p.addText(strings.Replace(p.tok.Data, "\x00", "", -1))
-	case StartTagToken:
-		switch p.tok.DataAtom {
-		case a.Html:
-			return inBodyIM(p)
-		case a.Option:
-			if p.top().DataAtom == a.Option {
-				p.oe.pop()
-			}
-			p.addElement()
-		case a.Optgroup:
-			if p.top().DataAtom == a.Option {
-				p.oe.pop()
-			}
-			if p.top().DataAtom == a.Optgroup {
-				p.oe.pop()
-			}
-			p.addElement()
-		case a.Select:
-			if !p.popUntil(selectScope, a.Select) {
-				// Ignore the token.
-				return true
-			}
-			p.resetInsertionMode()
-		case a.Input, a.Keygen, a.Textarea:
-			if p.elementInScope(selectScope, a.Select) {
-				p.parseImpliedToken(EndTagToken, a.Select, a.Select.String())
-				return false
-			}
-			// In order to properly ignore <textarea>, we need to change the tokenizer mode.
-			p.tokenizer.NextIsNotRawText()
-			// Ignore the token.
-			return true
-		case a.Script, a.Template:
-			return inHeadIM(p)
-		case a.Iframe, a.Noembed, a.Noframes, a.Noscript, a.Plaintext, a.Style, a.Title, a.Xmp:
-			// Don't let the tokenizer go into raw text mode when there are raw tags
-			// to be ignored. These tags should be ignored from the tokenizer
-			// properly.
-			p.tokenizer.NextIsNotRawText()
-			// Ignore the token.
-			return true
-		}
-	case EndTagToken:
-		switch p.tok.DataAtom {
-		case a.Option:
-			if p.top().DataAtom == a.Option {
-				p.oe.pop()
-			}
-		case a.Optgroup:
-			i := len(p.oe) - 1
-			if p.oe[i].DataAtom == a.Option {
-				i--
-			}
-			if p.oe[i].DataAtom == a.Optgroup {
-				p.oe = p.oe[:i]
-			}
-		case a.Select:
-			if !p.popUntil(selectScope, a.Select) {
-				// Ignore the token.
-				return true
-			}
-			p.resetInsertionMode()
-		case a.Template:
-			return inHeadIM(p)
-		}
-	case CommentToken:
-		p.addChild(&Node{
-			Type: CommentNode,
-			Data: p.tok.Data,
-		})
-	case DoctypeToken:
-		// Ignore the token.
-		return true
-	case ErrorToken:
-		return inBodyIM(p)
-	}
-
-	return true
-}
-
-// Section 12.2.6.4.17.
-func inSelectInTableIM(p *parser) bool {
-	switch p.tok.Type {
-	case StartTagToken, EndTagToken:
-		switch p.tok.DataAtom {
-		case a.Caption, a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr, a.Td, a.Th:
-			if p.tok.Type == EndTagToken && !p.elementInScope(tableScope, p.tok.DataAtom) {
-				// Ignore the token.
-				return true
-			}
-			// This is like p.popUntil(selectScope, a.Select), but it also
-			// matches <math select>, not just <select>. Matching the MathML
-			// tag is arguably incorrect (conceptually), but it mimics what
-			// Chromium does.
-			for i := len(p.oe) - 1; i >= 0; i-- {
-				if n := p.oe[i]; n.DataAtom == a.Select {
-					p.oe = p.oe[:i]
-					break
-				}
-			}
-			p.resetInsertionMode()
-			return false
-		}
-	}
-	return inSelectIM(p)
-}
-
 // Section 12.2.6.4.18.
 func inTemplateIM(p *parser) bool {
 	switch p.tok.Type {
@@ -2174,7 +2061,7 @@
 
 const whitespaceOrNUL = whitespace + "\x00"
 
-// Section 12.2.6.5
+// Section 13.2.6.5
 func parseForeignContent(p *parser) bool {
 	switch p.tok.Type {
 	case TextToken:
@@ -2189,28 +2076,26 @@
 			Data: p.tok.Data,
 		})
 	case StartTagToken:
-		if !p.fragment {
-			b := breakout[p.tok.Data]
-			if p.tok.DataAtom == a.Font {
-			loop:
-				for _, attr := range p.tok.Attr {
-					switch attr.Key {
-					case "color", "face", "size":
-						b = true
-						break loop
-					}
+		b := breakout[p.tok.Data]
+		if p.tok.DataAtom == a.Font {
+		loop:
+			for _, attr := range p.tok.Attr {
+				switch attr.Key {
+				case "color", "face", "size":
+					b = true
+					break loop
 				}
 			}
-			if b {
-				for i := len(p.oe) - 1; i >= 0; i-- {
-					n := p.oe[i]
-					if n.Namespace == "" || htmlIntegrationPoint(n) || mathMLTextIntegrationPoint(n) {
-						p.oe = p.oe[:i+1]
-						break
-					}
+		}
+		if b {
+			for i := len(p.oe) - 1; i >= 0; i-- {
+				n := p.oe[i]
+				if n.Namespace == "" || htmlIntegrationPoint(n) || mathMLTextIntegrationPoint(n) {
+					p.oe = p.oe[:i+1]
+					break
 				}
-				return false
 			}
+			return p.im(p)
 		}
 		current := p.adjustedCurrentNode()
 		switch current.Namespace {
diff --git a/html/parse_test.go b/html/parse_test.go
index cafa34d..8f2b49b 100644
--- a/html/parse_test.go
+++ b/html/parse_test.go
@@ -152,9 +152,6 @@
 }
 
 func (a sortedAttributes) Less(i, j int) bool {
-	if a[i].Namespace != a[j].Namespace {
-		return a[i].Namespace < a[j].Namespace
-	}
 	return a[i].Key < a[j].Key
 }
 
@@ -242,7 +239,7 @@
 	return b.String(), nil
 }
 
-var testDataDirs = []string{"testdata/webkit/", "testdata/go/"}
+var testDataDirs = []string{"testdata/html5lib-tests/tree-construction/", "testdata/go/"}
 
 func TestParser(t *testing.T) {
 	for _, testDataDir := range testDataDirs {
@@ -251,35 +248,33 @@
 			t.Fatal(err)
 		}
 		for _, tf := range testFiles {
-			t.Run(tf, func(t *testing.T) {
-				f, err := os.Open(tf)
+			f, err := os.Open(tf)
+			if err != nil {
+				t.Fatal(err)
+			}
+			defer f.Close()
+			r := bufio.NewReader(f)
+
+			for i := 0; ; i++ {
+				ta, err := readParseTest(r)
+				if err == io.EOF {
+					break
+				}
 				if err != nil {
 					t.Fatal(err)
 				}
-				defer f.Close()
-				r := bufio.NewReader(f)
-
-				for i := 0; ; i++ {
-					ta, err := readParseTest(r)
-					if err == io.EOF {
-						break
-					}
-					if err != nil {
-						t.Fatal(err)
-					}
-					if parseTestBlacklist[ta.text] {
-						continue
-					}
-
-					t.Run(fmt.Sprint(i), func(t *testing.T) {
-						err = testParseCase(ta.text, ta.want, ta.context, ParseOptionEnableScripting(ta.scripting))
-
-						if err != nil {
-							t.Errorf("%s test #%d %q, %s", tf, i, ta.text, err)
-						}
-					})
+				if parseTestBlacklist[ta.text] {
+					continue
 				}
-			})
+
+				testId := fmt.Sprintf("%s/%d", strings.ReplaceAll(tf, string(os.PathSeparator), "/"), i)
+				t.Run(testId, func(t *testing.T) {
+					err = testParseCase(ta.text, ta.want, ta.context, ParseOptionEnableScripting(ta.scripting))
+					if err != nil {
+						t.Errorf("%s test #%d %q, %s", tf, i, ta.text, err)
+					}
+				})
+			}
 		}
 	}
 }
@@ -391,6 +386,11 @@
 	// See the a.Template TODO in inHeadIM.
 	`<math><template><mo><template>`:                                     true,
 	`<template><svg><foo><template><foreignObject><div></template><div>`: true,
+	// We don't support "element insertion steps"
+	`<select><button><selectedcontent></button><option>X`:                   true,
+	`<select><button><selectedcontent></button><option>x<i>i<b>ib</i>b`:     true,
+	`<select><button><selectedcontent></button><option>X<option>Y`:          true,
+	`<select><button><selectedcontent></button><option>X<option selected>Y`: true,
 }
 
 // Some test input result in parse trees are not 'well-formed' despite
@@ -451,6 +451,8 @@
 	`<!doctype html><svg><plaintext>a</plaintext>b`:           true,
 	// Due to fostering, parsing the rendered output produces a different tree.
 	`<math><mtext><table><mglyph><style><img>`: true,
+	// Confusing plaintext behavior
+	`<!doctype html><table><select><plaintext>a<caption>b`: true,
 }
 
 func TestNodeConsistency(t *testing.T) {
diff --git a/html/testdata/go/raw_tags_to_be_ignored.dat b/html/testdata/go/raw_tags_to_be_ignored.dat
deleted file mode 100644
index 50bac59..0000000
--- a/html/testdata/go/raw_tags_to_be_ignored.dat
+++ /dev/null
@@ -1,97 +0,0 @@
-#data
-<!doctype html><table><select><iframe>a<caption>b
-#errors
-#document
-| <!DOCTYPE html>
-| <html>
-|   <head>
-|   <body>
-|     <select>
-|       "a"
-|     <table>
-|       <caption>
-|         "b"
-
-#data
-<!doctype html><table><select><noembed>a<caption>b
-#errors
-#document
-| <!DOCTYPE html>
-| <html>
-|   <head>
-|   <body>
-|     <select>
-|       "a"
-|     <table>
-|       <caption>
-|         "b"
-
-#data
-<!doctype html><table><select><noframes>a<caption>b
-#errors
-#document
-| <!DOCTYPE html>
-| <html>
-|   <head>
-|   <body>
-|     <select>
-|       "a"
-|     <table>
-|       <caption>
-|         "b"
-
-#data
-<!doctype html><table><select><noscript>a<caption>b
-#errors
-#document
-| <!DOCTYPE html>
-| <html>
-|   <head>
-|   <body>
-|     <select>
-|       "a"
-|     <table>
-|       <caption>
-|         "b"
-
-#data
-<!doctype html><table><select><style>a<caption>b
-#errors
-#document
-| <!DOCTYPE html>
-| <html>
-|   <head>
-|   <body>
-|     <select>
-|       "a"
-|     <table>
-|       <caption>
-|         "b"
-
-#data
-<!doctype html><table><select><title>a<caption>b
-#errors
-#document
-| <!DOCTYPE html>
-| <html>
-|   <head>
-|   <body>
-|     <select>
-|       "a"
-|     <table>
-|       <caption>
-|         "b"
-
-#data
-<!doctype html><table><select><xmp>a<caption>b
-#errors
-#document
-| <!DOCTYPE html>
-| <html>
-|   <head>
-|   <body>
-|     <select>
-|       "a"
-|     <table>
-|       <caption>
-|         "b"
diff --git a/html/testdata/html5lib-tests/tree-construction/README.md b/html/testdata/html5lib-tests/tree-construction/README.md
new file mode 100644
index 0000000..4737a3a
--- /dev/null
+++ b/html/testdata/html5lib-tests/tree-construction/README.md
@@ -0,0 +1,108 @@
+Tree Construction Tests
+=======================
+
+Each file containing tree construction tests consists of any number of
+tests separated by two newlines (LF) and a single newline before the end
+of the file. For instance:
+
+    [TEST]LF
+    LF
+    [TEST]LF
+    LF
+    [TEST]LF
+
+Where [TEST] is the following format:
+
+Each test must begin with a string "\#data" followed by a newline (LF).
+All subsequent lines until a line that says "\#errors" are the test data
+and must be passed to the system being tested unchanged, except with the
+final newline (on the last line) removed.
+
+Then there must be a line that says "\#errors". It must be followed by
+one line per parse error that a conformant checker would return. It
+doesn't matter what those lines are, although they can't be
+"\#new-errors", "\#document-fragment", "\#document", "\#script-off",
+"\#script-on", or empty, the only thing that matters is that there be
+the right number of parse errors.
+
+Then there \*may\* be a line that says "\#new-errors", which works like
+the "\#errors" section adding more errors to the expected number of
+errors.
+
+Then there \*may\* be a line that says "\#document-fragment", which must
+be followed by a newline (LF), followed by a string of characters that
+indicates the context element, followed by a newline (LF). If the string 
+of characters starts with "svg ", the context element is in the SVG
+namespace and the substring after "svg " is the local name. If the
+string of characters starts with "math ", the context element is in the
+MathML namespace and the substring after "math " is the local name.
+Otherwise, the context element is in the HTML namespace and the string
+is the local name. If this line is present the "\#data" must be parsed
+using the HTML fragment parsing algorithm with the context element as
+context.
+
+Then there \*may\* be a line that says "\#script-off" or
+"\#script-on". If a line that says "\#script-off" is present, the
+parser must set the scripting flag to disabled. If a line that says
+"\#script-on" is present, it must set it to enabled. Otherwise, the
+test should be run in both modes.
+
+Then there must be a line that says "\#document", which must be followed
+by a dump of the tree of the parsed DOM. Each node must be represented
+by a single line. Each line must start with "| ", followed by two spaces
+per parent node that the node has before the root document node.
+
+-   Element nodes must be represented by a "`<`" then the *tag name
+    string* "`>`", and all the attributes must be given, sorted
+    lexicographically by UTF-16 code unit according to their *attribute
+    name string*, on subsequent lines, as if they were children of the
+    element node.
+-   Attribute nodes must have the *attribute name string*, then an "="
+    sign, then the attribute value in double quotes (").
+-   Text nodes must be the string, in double quotes. Newlines aren't
+    escaped.
+-   Comments must be "`<`" then "`!-- `" then the data then "` -->`".
+-   DOCTYPEs must be "`<!DOCTYPE `" then the name then if either of the
+    system id or public id is non-empty a space, public id in
+    double-quotes, another space an the system id in double-quotes, and
+    then in any case "`>`".
+-   Processing instructions must be "`<?`", then the target, then a
+    space, then the data and then "`>`". (The HTML parser cannot emit
+    processing instructions, but scripts can, and the WebVTT to DOM
+    rules can emit them.)
+-   Template contents are represented by the string "content" with the
+    children below it.
+
+The *tag name string* is the local name prefixed by a namespace
+designator. For the HTML namespace, the namespace designator is the
+empty string, i.e. there's no prefix. For the SVG namespace, the
+namespace designator is "svg ". For the MathML namespace, the namespace
+designator is "math ".
+
+The *attribute name string* is the local name prefixed by a namespace
+designator. For no namespace, the namespace designator is the empty
+string, i.e. there's no prefix. For the XLink namespace, the namespace
+designator is "xlink ". For the XML namespace, the namespace designator
+is "xml ". For the XMLNS namespace, the namespace designator is "xmlns
+". Note the difference between "xlink:href" which is an attribute in no
+namespace with the local name "xlink:href" and "xlink href" which is an
+attribute in the xlink namespace with the local name "href".
+
+If there is also a "\#document-fragment" the bit following "\#document"
+must be a representation of the HTML fragment serialization for the
+context element given by "\#document-fragment".
+
+For example:
+
+    #data
+    <p>One<p>Two
+    #errors
+    3: Missing document type declaration
+    #document
+    | <html>
+    |   <head>
+    |   <body>
+    |     <p>
+    |       "One"
+    |     <p>
+    |       "Two"
diff --git a/html/testdata/webkit/adoption01.dat b/html/testdata/html5lib-tests/tree-construction/adoption01.dat
similarity index 100%
rename from html/testdata/webkit/adoption01.dat
rename to html/testdata/html5lib-tests/tree-construction/adoption01.dat
diff --git a/html/testdata/webkit/adoption02.dat b/html/testdata/html5lib-tests/tree-construction/adoption02.dat
similarity index 100%
rename from html/testdata/webkit/adoption02.dat
rename to html/testdata/html5lib-tests/tree-construction/adoption02.dat
diff --git a/html/testdata/webkit/blocks.dat b/html/testdata/html5lib-tests/tree-construction/blocks.dat
similarity index 91%
rename from html/testdata/webkit/blocks.dat
rename to html/testdata/html5lib-tests/tree-construction/blocks.dat
index 5d3871e..a1a9c75 100644
--- a/html/testdata/webkit/blocks.dat
+++ b/html/testdata/html5lib-tests/tree-construction/blocks.dat
@@ -2,7 +2,6 @@
 <!doctype html><p>foo<address>bar<p>baz
 #errors
 (1,39): expected-closing-tag-but-got-eof
-30: Unclosed element “address”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -32,7 +31,6 @@
 <!doctype html><p>foo<article>bar<p>baz
 #errors
 (1,39): expected-closing-tag-but-got-eof
-30: Unclosed element “article”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -62,7 +60,6 @@
 <!doctype html><p>foo<aside>bar<p>baz
 #errors
 (1,37): expected-closing-tag-but-got-eof
-28: Unclosed element “aside”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -92,7 +89,6 @@
 <!doctype html><p>foo<blockquote>bar<p>baz
 #errors
 (1,42): expected-closing-tag-but-got-eof
-33: Unclosed element “blockquote”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -122,7 +118,6 @@
 <!doctype html><p>foo<center>bar<p>baz
 #errors
 (1,38): expected-closing-tag-but-got-eof
-29: Unclosed element “center”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -152,7 +147,6 @@
 <!doctype html><p>foo<details>bar<p>baz
 #errors
 (1,39): expected-closing-tag-but-got-eof
-30: Unclosed element “details”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -182,7 +176,6 @@
 <!doctype html><p>foo<dialog>bar<p>baz
 #errors
 (1,38): expected-closing-tag-but-got-eof
-29: Unclosed element “dialog”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -212,7 +205,6 @@
 <!doctype html><p>foo<dir>bar<p>baz
 #errors
 (1,35): expected-closing-tag-but-got-eof
-26: Unclosed element “dir”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -242,7 +234,6 @@
 <!doctype html><p>foo<div>bar<p>baz
 #errors
 (1,35): expected-closing-tag-but-got-eof
-26: Unclosed element “div”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -272,7 +263,6 @@
 <!doctype html><p>foo<dl>bar<p>baz
 #errors
 (1,34): expected-closing-tag-but-got-eof
-25: Unclosed element “dl”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -302,7 +292,6 @@
 <!doctype html><p>foo<fieldset>bar<p>baz
 #errors
 (1,40): expected-closing-tag-but-got-eof
-31: Unclosed element “fieldset”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -332,7 +321,6 @@
 <!doctype html><p>foo<figcaption>bar<p>baz
 #errors
 (1,42): expected-closing-tag-but-got-eof
-33: Unclosed element “figcaption”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -362,7 +350,6 @@
 <!doctype html><p>foo<figure>bar<p>baz
 #errors
 (1,38): expected-closing-tag-but-got-eof
-29: Unclosed element “figure”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -392,7 +379,6 @@
 <!doctype html><p>foo<footer>bar<p>baz
 #errors
 (1,38): expected-closing-tag-but-got-eof
-29: Unclosed element “footer”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -422,7 +408,6 @@
 <!doctype html><p>foo<header>bar<p>baz
 #errors
 (1,38): expected-closing-tag-but-got-eof
-29: Unclosed element “header”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -452,7 +437,6 @@
 <!doctype html><p>foo<hgroup>bar<p>baz
 #errors
 (1,38): expected-closing-tag-but-got-eof
-29: Unclosed element “hgroup”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -482,7 +466,6 @@
 <!doctype html><p>foo<listing>bar<p>baz
 #errors
 (1,39): expected-closing-tag-but-got-eof
-30: Unclosed element “listing”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -512,7 +495,6 @@
 <!doctype html><p>foo<menu>bar<p>baz
 #errors
 (1,36): expected-closing-tag-but-got-eof
-27: Unclosed element “menu”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -542,7 +524,6 @@
 <!doctype html><p>foo<nav>bar<p>baz
 #errors
 (1,35): expected-closing-tag-but-got-eof
-26: Unclosed element “nav”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -572,7 +553,6 @@
 <!doctype html><p>foo<ol>bar<p>baz
 #errors
 (1,34): expected-closing-tag-but-got-eof
-25: Unclosed element “ol”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -602,7 +582,6 @@
 <!doctype html><p>foo<pre>bar<p>baz
 #errors
 (1,35): expected-closing-tag-but-got-eof
-26: Unclosed element “pre”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -632,7 +611,6 @@
 <!doctype html><p>foo<section>bar<p>baz
 #errors
 (1,39): expected-closing-tag-but-got-eof
-30: Unclosed element “section”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -662,7 +640,6 @@
 <!doctype html><p>foo<summary>bar<p>baz
 #errors
 (1,39): expected-closing-tag-but-got-eof
-30: Unclosed element “summary”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -692,7 +669,6 @@
 <!doctype html><p>foo<ul>bar<p>baz
 #errors
 (1,34): expected-closing-tag-but-got-eof
-25: Unclosed element “ul”.
 #document
 | <!DOCTYPE html>
 | <html>
diff --git a/html/testdata/webkit/comments01.dat b/html/testdata/html5lib-tests/tree-construction/comments01.dat
similarity index 89%
rename from html/testdata/webkit/comments01.dat
rename to html/testdata/html5lib-tests/tree-construction/comments01.dat
index fa79c2b..4b9ff95 100644
--- a/html/testdata/webkit/comments01.dat
+++ b/html/testdata/html5lib-tests/tree-construction/comments01.dat
@@ -29,8 +29,9 @@
 FOO<!-- BAR --! >BAZ
 #errors
 (1,3): expected-doctype-but-got-chars
+(1:21) eof-in-comment
 #new-errors
-(1:20) eof-in-comment
+(1:21) eof-in-comment
 #document
 | <html>
 |   <head>
@@ -43,8 +44,9 @@
 >BAZ
 #errors
 (1,3): expected-doctype-but-got-chars
+(2:5) eof-in-comment
 #new-errors
-(1:20) eof-in-comment
+(2:5) eof-in-comment
 #document
 | <html>
 |   <head>
@@ -57,7 +59,6 @@
 FOO<!-- BAR --   >BAZ
 #errors
 (1,3): expected-doctype-but-got-chars
-(1,15): unexpected-char-in-comment
 (1,21): eof-in-comment
 #new-errors
 (1:22) eof-in-comment
@@ -72,8 +73,6 @@
 FOO<!-- BAR -- <QUX> -- MUX -->BAZ
 #errors
 (1,3): expected-doctype-but-got-chars
-(1,15): unexpected-char-in-comment
-(1,24): unexpected-char-in-comment
 #document
 | <html>
 |   <head>
@@ -86,8 +85,6 @@
 FOO<!-- BAR -- <QUX> -- MUX --!>BAZ
 #errors
 (1,3): expected-doctype-but-got-chars
-(1,15): unexpected-char-in-comment
-(1,24): unexpected-char-in-comment
 (1,31): unexpected-bang-after-double-dash-in-comment
 #new-errors
 (1:32) incorrectly-closed-comment
@@ -103,9 +100,6 @@
 FOO<!-- BAR -- <QUX> -- MUX -- >BAZ
 #errors
 (1,3): expected-doctype-but-got-chars
-(1,15): unexpected-char-in-comment
-(1,24): unexpected-char-in-comment
-(1,31): unexpected-char-in-comment
 (1,35): eof-in-comment
 #new-errors
 (1:36) eof-in-comment
@@ -202,7 +196,6 @@
 FOO<!----->BAZ
 #errors
 (1,3): expected-doctype-but-got-chars
-(1,10): unexpected-dash-after-double-dash-in-comment
 #document
 | <html>
 |   <head>
diff --git a/html/testdata/webkit/doctype01.dat b/html/testdata/html5lib-tests/tree-construction/doctype01.dat
similarity index 98%
rename from html/testdata/webkit/doctype01.dat
rename to html/testdata/html5lib-tests/tree-construction/doctype01.dat
index c845bec..9efdaf7 100644
--- a/html/testdata/webkit/doctype01.dat
+++ b/html/testdata/html5lib-tests/tree-construction/doctype01.dat
@@ -34,7 +34,6 @@
 #data
 <!DOCTYPE>Hello
 #errors
-(1,9): need-space-after-doctype
 (1,10): expected-doctype-name-but-got-right-bracket
 (1,10): unknown-doctype
 #new-errors
@@ -337,6 +336,7 @@
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
    "http://www.w3.org/TR/html4/strict.dtd">Hello
 #errors
+(2,43): unknown-doctype
 #document
 | <!DOCTYPE html "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
 | <html>
@@ -421,6 +421,7 @@
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
 #errors
 (1,50): unexpected-char-in-doctype
+(1,89): unknown-doctype
 #new-errors
 (1:50) missing-whitespace-between-doctype-public-and-system-identifiers
 #document
@@ -433,6 +434,7 @@
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"'http://www.w3.org/TR/html4/strict.dtd'>
 #errors
 (1,50): unexpected-char-in-doctype
+(1,89): unknown-doctype
 #new-errors
 (1:50) missing-whitespace-between-doctype-public-and-system-identifiers
 #document
@@ -446,6 +448,7 @@
 #errors
 (1,21): unexpected-char-in-doctype
 (1,49): unexpected-char-in-doctype
+(1,88): unknown-doctype
 #new-errors
 (1:22) missing-whitespace-after-doctype-public-keyword
 (1:49) missing-whitespace-between-doctype-public-and-system-identifiers
@@ -460,6 +463,7 @@
 #errors
 (1,21): unexpected-char-in-doctype
 (1,49): unexpected-char-in-doctype
+(1,88): unknown-doctype
 #new-errors
 (1:22) missing-whitespace-after-doctype-public-keyword
 (1:49) missing-whitespace-between-doctype-public-and-system-identifiers
diff --git a/html/testdata/webkit/domjs-unsafe.dat b/html/testdata/html5lib-tests/tree-construction/domjs-unsafe.dat
similarity index 100%
rename from html/testdata/webkit/domjs-unsafe.dat
rename to html/testdata/html5lib-tests/tree-construction/domjs-unsafe.dat
Binary files differ
diff --git a/html/testdata/webkit/entities01.dat b/html/testdata/html5lib-tests/tree-construction/entities01.dat
similarity index 100%
rename from html/testdata/webkit/entities01.dat
rename to html/testdata/html5lib-tests/tree-construction/entities01.dat
diff --git a/html/testdata/webkit/entities02.dat b/html/testdata/html5lib-tests/tree-construction/entities02.dat
similarity index 97%
rename from html/testdata/webkit/entities02.dat
rename to html/testdata/html5lib-tests/tree-construction/entities02.dat
index 0c6e898..74965a3 100644
--- a/html/testdata/webkit/entities02.dat
+++ b/html/testdata/html5lib-tests/tree-construction/entities02.dat
@@ -45,7 +45,6 @@
 #data
 <div bar="ZZ&gt=YY"></div>
 #errors
-(1,15): named-entity-without-semicolon
 (1,20): expected-doctype-but-got-start-tag
 #document
 | <html>
@@ -204,7 +203,6 @@
 #data
 <div bar="ZZ&pound=23"></div>
 #errors
-(1,18): named-entity-without-semicolon
 (1,23): expected-doctype-but-got-start-tag
 #document
 | <html>
@@ -299,6 +297,8 @@
 #data
 <div>ZZ&AElig=</div>
 #errors
+(1,5): expected-doctype-but-got-start-tag
+(1:14) missing-semicolon-after-character-reference
 #new-errors
 (1:14) missing-semicolon-after-character-reference
 #document
diff --git a/html/testdata/webkit/foreign-fragment.dat b/html/testdata/html5lib-tests/tree-construction/foreign-fragment.dat
similarity index 81%
rename from html/testdata/webkit/foreign-fragment.dat
rename to html/testdata/html5lib-tests/tree-construction/foreign-fragment.dat
index c81ae81..e562c6b 100644
--- a/html/testdata/webkit/foreign-fragment.dat
+++ b/html/testdata/html5lib-tests/tree-construction/foreign-fragment.dat
@@ -3,11 +3,10 @@
 #errors
 6: HTML start tag “nobr” in a foreign namespace context.
 7: End of file seen and there were open elements.
-6: Unclosed element “nobr”.
 #document-fragment
 svg path
 #document
-| <svg nobr>
+| <nobr>
 |   "X"
 
 #data
@@ -17,7 +16,7 @@
 #document-fragment
 svg path
 #document
-| <svg font>
+| <font>
 |   color=""
 | "X"
 
@@ -35,7 +34,6 @@
 #errors
 10: End tag “path” did not match the name of the current open element (“g”).
 11: End of file seen and there were open elements.
-3: Unclosed element “g”.
 #document-fragment
 svg path
 #document
@@ -173,7 +171,6 @@
 #errors
 51: Self-closing syntax (“/>”) used on a non-void HTML element. Ignoring the slash and treating as a start tag.
 52: End of file seen and there were open elements.
-51: Unclosed element “ms”.
 #new-errors
 (1:44-1:49) non-void-html-element-start-tag-with-trailing-solidus
 #document-fragment
@@ -216,7 +213,6 @@
 #errors
 51: Self-closing syntax (“/>”) used on a non-void HTML element. Ignoring the slash and treating as a start tag.
 52: End of file seen and there were open elements.
-51: Unclosed element “mn”.
 #new-errors
 (1:44-1:49) non-void-html-element-start-tag-with-trailing-solidus
 #document-fragment
@@ -259,7 +255,6 @@
 #errors
 51: Self-closing syntax (“/>”) used on a non-void HTML element. Ignoring the slash and treating as a start tag.
 52: End of file seen and there were open elements.
-51: Unclosed element “mo”.
 #new-errors
 (1:44-1:49) non-void-html-element-start-tag-with-trailing-solidus
 #document-fragment
@@ -302,7 +297,6 @@
 #errors
 51: Self-closing syntax (“/>”) used on a non-void HTML element. Ignoring the slash and treating as a start tag.
 52: End of file seen and there were open elements.
-51: Unclosed element “mi”.
 #new-errors
 (1:44-1:49) non-void-html-element-start-tag-with-trailing-solidus
 #document-fragment
@@ -345,7 +339,6 @@
 #errors
 51: Self-closing syntax (“/>”) used on a non-void HTML element. Ignoring the slash and treating as a start tag.
 52: End of file seen and there were open elements.
-51: Unclosed element “mtext”.
 #new-errors
 (1:44-1:52) non-void-html-element-start-tag-with-trailing-solidus
 #document-fragment
@@ -390,7 +383,7 @@
 #document-fragment
 math annotation-xml
 #document
-| <math div>
+| <div>
 
 #data
 <figure></figure>
@@ -407,7 +400,7 @@
 #document-fragment
 math math
 #document
-| <math div>
+| <div>
 
 #data
 <figure></figure>
@@ -461,12 +454,11 @@
 <div><h1>X</h1></div>
 #errors
 5: HTML start tag “div” in a foreign namespace context.
-9: HTML start tag “h1” in a foreign namespace context.
 #document-fragment
 svg svg
 #document
-| <svg div>
-|   <svg h1>
+| <div>
+|   <h1>
 |     "X"
 
 #data
@@ -476,7 +468,7 @@
 #document-fragment
 svg svg
 #document
-| <svg div>
+| <div>
 
 #data
 <div></div>
@@ -487,14 +479,6 @@
 | <div>
 
 #data
-<figure></figure>
-#errors
-#document-fragment
-svg desc
-#document
-| <figure>
-
-#data
 <plaintext><foo>
 #errors
 (1,16): expected-closing-tag-but-got-eof
@@ -557,3 +541,105 @@
 svg desc
 #document
 | "X"
+
+#data
+<svg><p>
+#errors
+8: HTML start tag “p” in a foreign namespace context.
+#document-fragment
+div
+#document
+| <svg svg>
+| <p>
+
+#data
+<p>
+#errors
+3: HTML start tag “p” in a foreign namespace context.
+#document-fragment
+svg svg
+#document
+| <p>
+
+#data
+<svg></p><foo>
+#errors
+9: HTML end tag “p” in a foreign namespace context.
+(1:6) Unexpected </p> from in body insertion mode
+(1:15) Unexpected EOF
+#document-fragment
+div
+#document
+| <svg svg>
+| <p>
+| <foo>
+
+#data
+<svg></br><foo>
+#errors
+10: HTML end tag “br” in a foreign namespace context.
+(1:6) Unexpected </br> from in body insertion mode
+(1:16) Unexpected EOF
+#document-fragment
+div
+#document
+| <svg svg>
+| <br>
+| <foo>
+
+#data
+</p><foo>
+#errors
+4: HTML end tag “p” in a foreign namespace context.
+(1:1) Unexpected </p> from in body insertion mode
+(1:10) Unexpected EOF
+#document-fragment
+svg svg
+#document
+| <p>
+| <svg foo>
+
+#data
+</br><foo>
+#errors
+5: HTML end tag “br” in a foreign namespace context.
+(1:1) Unexpected </br> from in body insertion mode
+(1:11) Unexpected EOF
+#document-fragment
+svg svg
+#document
+| <br>
+| <svg foo>
+
+#data
+<body><foo>
+#errors
+6: HTML start tag “body” in a foreign namespace context.
+(1:1) Unexpected <body> from in body insertion mode
+(1:12) Unexpected EOF
+#document-fragment
+svg svg
+#document
+| <svg foo>
+
+#data
+<p><foo>
+#errors
+3: HTML start tag “p” in a foreign namespace context.
+(1:9) Unexpected EOF
+#document-fragment
+svg svg
+#document
+| <p>
+|   <foo>
+
+#data
+<p></p><foo>
+#errors
+3: HTML start tag “p” in a foreign namespace context.
+(1:13) Unexpected EOF
+#document-fragment
+svg svg
+#document
+| <p>
+| <svg foo>
diff --git a/html/testdata/webkit/html5test-com.dat b/html/testdata/html5lib-tests/tree-construction/html5test-com.dat
similarity index 98%
rename from html/testdata/webkit/html5test-com.dat
rename to html/testdata/html5lib-tests/tree-construction/html5test-com.dat
index f738010..48d0bf9 100644
--- a/html/testdata/webkit/html5test-com.dat
+++ b/html/testdata/html5lib-tests/tree-construction/html5test-com.dat
@@ -142,7 +142,6 @@
 #data
 <!--foo--bar-->
 #errors
-(1,10): unexpected-char-in-comment
 (1,15): expected-doctype-but-got-eof
 #document
 | <!-- foo--bar -->
diff --git a/html/testdata/webkit/inbody01.dat b/html/testdata/html5lib-tests/tree-construction/inbody01.dat
similarity index 100%
rename from html/testdata/webkit/inbody01.dat
rename to html/testdata/html5lib-tests/tree-construction/inbody01.dat
diff --git a/html/testdata/webkit/isindex.dat b/html/testdata/html5lib-tests/tree-construction/isindex.dat
similarity index 100%
rename from html/testdata/webkit/isindex.dat
rename to html/testdata/html5lib-tests/tree-construction/isindex.dat
diff --git a/html/testdata/webkit/main-element.dat b/html/testdata/html5lib-tests/tree-construction/main-element.dat
similarity index 100%
rename from html/testdata/webkit/main-element.dat
rename to html/testdata/html5lib-tests/tree-construction/main-element.dat
diff --git a/html/testdata/html5lib-tests/tree-construction/math.dat b/html/testdata/html5lib-tests/tree-construction/math.dat
new file mode 100644
index 0000000..d6a8ae5
--- /dev/null
+++ b/html/testdata/html5lib-tests/tree-construction/math.dat
@@ -0,0 +1,104 @@
+#data
+<math><tr><td><mo><tr>
+#errors
+(1,22): unexpected-start-tag
+(1,23): expected-closing-tag-but-got-eof
+#document-fragment
+td
+#document
+| <math math>
+|   <math tr>
+|     <math td>
+|       <math mo>
+
+#data
+<math><tr><td><mo><tr>
+#errors
+(1,6): foster-parenting-start-tag
+(1,22): expected-tr-in-table-scope
+(1,23): expected-closing-tag-but-got-eof
+#document-fragment
+tr
+#document
+| <math math>
+|   <math tr>
+|     <math td>
+|       <math mo>
+
+#data
+<math><thead><mo><tbody>
+#errors
+(1,6): foster-parenting-start-tag
+(1,24): expected-table-part-in-table-scope
+(1,25): expected-closing-tag-but-got-eof
+#document-fragment
+thead
+#document
+| <math math>
+|   <math thead>
+|     <math mo>
+
+#data
+<math><tfoot><mo><tbody>
+#errors
+(1,6): foster-parenting-start-tag
+(1,24): expected-table-part-in-table-scope
+(1,25): expected-closing-tag-but-got-eof
+#document-fragment
+tfoot
+#document
+| <math math>
+|   <math tfoot>
+|     <math mo>
+
+#data
+<math><tbody><mo><tfoot>
+#errors
+(1,6): foster-parenting-start-tag
+(1,24): expected-table-part-in-table-scope
+(1,25): expected-closing-tag-but-got-eof
+#document-fragment
+tbody
+#document
+| <math math>
+|   <math tbody>
+|     <math mo>
+
+#data
+<math><tbody><mo></table>
+#errors
+(1,6): foster-parenting-start-tag
+(1,25): unexpected-end-tag-in-math
+(1,26): expected-closing-tag-but-got-eof
+#document-fragment
+tbody
+#document
+| <math math>
+|   <math tbody>
+|     <math mo>
+
+#data
+<math><thead><mo></table>
+#errors
+(1,6): foster-parenting-start-tag
+(1,25): unexpected-end-tag-in-math
+(1,26): expected-closing-tag-but-got-eof
+#document-fragment
+tbody
+#document
+| <math math>
+|   <math thead>
+|     <math mo>
+
+#data
+<math><tfoot><mo></table>
+#errors
+(1,6): foster-parenting-start-tag
+(1,25): unexpected-end-tag-in-math
+(1,26): expected-closing-tag-but-got-eof
+#document-fragment
+tbody
+#document
+| <math math>
+|   <math tfoot>
+|     <math mo>
diff --git a/html/testdata/webkit/menuitem-element.dat b/html/testdata/html5lib-tests/tree-construction/menuitem-element.dat
similarity index 85%
rename from html/testdata/webkit/menuitem-element.dat
rename to html/testdata/html5lib-tests/tree-construction/menuitem-element.dat
index 43aa0c6..f7c8e2c 100644
--- a/html/testdata/webkit/menuitem-element.dat
+++ b/html/testdata/html5lib-tests/tree-construction/menuitem-element.dat
@@ -3,7 +3,6 @@
 #errors
 10: Start tag seen without seeing a doctype first. Expected “<!DOCTYPE html>”.
 10: End of file seen and there were open elements.
-10: Unclosed element “menuitem”.
 #document
 | <html>
 |   <head>
@@ -24,7 +23,6 @@
 <!DOCTYPE html><body><menuitem>A
 #errors
 32: End of file seen and there were open elements.
-31: Unclosed element “menuitem”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -37,8 +35,6 @@
 <!DOCTYPE html><body><menuitem>A<menuitem>B
 #errors
 43: End of file seen and there were open elements.
-42: Unclosed element “menuitem”.
-31: Unclosed element “menuitem”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -53,7 +49,6 @@
 <!DOCTYPE html><body><menuitem>A<menu>B</menu>
 #errors
 46: End of file seen and there were open elements.
-31: Unclosed element “menuitem”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -68,7 +63,6 @@
 <!DOCTYPE html><body><menuitem>A<hr>B
 #errors
 37: End of file seen and there were open elements.
-31: Unclosed element “menuitem”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -83,7 +77,6 @@
 <!DOCTYPE html><li><menuitem><li>
 #errors
 33: End tag “li” implied, but there were open elements.
-29: Unclosed element “menuitem”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -98,7 +91,6 @@
 #errors
 39: Stray end tag “menuitem”.
 40: End of file seen and there were open elements.
-25: Unclosed element “menuitem”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -112,9 +104,7 @@
 <!DOCTYPE html><p><b></p><menuitem>
 #errors
 25: End tag “p” seen, but there were open elements.
-21: Unclosed element “b”.
 35: End of file seen and there were open elements.
-35: Unclosed element “menuitem”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -129,7 +119,6 @@
 <!DOCTYPE html><menuitem><asdf></menuitem>x
 #errors
 42: End tag “menuitem” seen, but there were open elements.
-31: Unclosed element “asdf”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -172,19 +161,19 @@
 #data
 <!DOCTYPE html><select><menuitem></select>
 #errors
-33: Stray start tag “menuitem”.
+1:34: ERROR: End tag 'select' isn't allowed here. Currently open tags: html, body, select, menuitem.
 #document
 | <!DOCTYPE html>
 | <html>
 |   <head>
 |   <body>
 |     <select>
+|       <menuitem>
 
 #data
 <!DOCTYPE html><option><menuitem>
 #errors
 33: End of file seen and there were open elements.
-33: Unclosed element “menuitem”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -197,7 +186,6 @@
 <!DOCTYPE html><menuitem><option>
 #errors
 33: End of file seen and there were open elements.
-25: Unclosed element “menuitem”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -210,7 +198,6 @@
 <!DOCTYPE html><menuitem></body>
 #errors
 32: End tag for  “body” seen, but there were unclosed elements.
-25: Unclosed element “menuitem”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -222,7 +209,6 @@
 <!DOCTYPE html><menuitem></html>
 #errors
 32: End tag for  “html” seen, but there were unclosed elements.
-25: Unclosed element “menuitem”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -234,7 +220,6 @@
 <!DOCTYPE html><menuitem><p>
 #errors
 28: End of file seen and there were open elements.
-25: Unclosed element “menuitem”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -247,7 +232,6 @@
 <!DOCTYPE html><menuitem><li>
 #errors
 29: End of file seen and there were open elements.
-25: Unclosed element “menuitem”.
 #document
 | <!DOCTYPE html>
 | <html>
diff --git a/html/testdata/html5lib-tests/tree-construction/namespace-sensitivity.dat b/html/testdata/html5lib-tests/tree-construction/namespace-sensitivity.dat
new file mode 100644
index 0000000..050dca7
--- /dev/null
+++ b/html/testdata/html5lib-tests/tree-construction/namespace-sensitivity.dat
@@ -0,0 +1,22 @@
+#data
+<body><table><tr><td><svg><td><foreignObject><span></td>Foo
+#errors
+(1,6): expected-doctype-but-got-start-tag
+(1,56): unexpected-end-tag
+(1,60): foster-parenting-character
+(1,60): foster-parenting-character
+(1,60): foster-parenting-character
+(1,60): expected-closing-tag-but-got-eof
+#document
+| <html>
+|   <head>
+|   <body>
+|     "Foo"
+|     <table>
+|       <tbody>
+|         <tr>
+|           <td>
+|             <svg svg>
+|               <svg td>
+|                 <svg foreignObject>
+|                   <span>
diff --git a/html/testdata/webkit/noscript01.dat b/html/testdata/html5lib-tests/tree-construction/noscript01.dat
similarity index 100%
rename from html/testdata/webkit/noscript01.dat
rename to html/testdata/html5lib-tests/tree-construction/noscript01.dat
diff --git a/html/testdata/webkit/pending-spec-changes-plain-text-unsafe.dat b/html/testdata/html5lib-tests/tree-construction/pending-spec-changes-plain-text-unsafe.dat
similarity index 100%
rename from html/testdata/webkit/pending-spec-changes-plain-text-unsafe.dat
rename to html/testdata/html5lib-tests/tree-construction/pending-spec-changes-plain-text-unsafe.dat
Binary files differ
diff --git a/html/testdata/webkit/pending-spec-changes.dat b/html/testdata/html5lib-tests/tree-construction/pending-spec-changes.dat
similarity index 100%
rename from html/testdata/webkit/pending-spec-changes.dat
rename to html/testdata/html5lib-tests/tree-construction/pending-spec-changes.dat
diff --git a/html/testdata/webkit/plain-text-unsafe.dat b/html/testdata/html5lib-tests/tree-construction/plain-text-unsafe.dat
similarity index 98%
rename from html/testdata/webkit/plain-text-unsafe.dat
rename to html/testdata/html5lib-tests/tree-construction/plain-text-unsafe.dat
index dfb5cb6..e904eff 100644
--- a/html/testdata/webkit/plain-text-unsafe.dat
+++ b/html/testdata/html5lib-tests/tree-construction/plain-text-unsafe.dat
Binary files differ
diff --git a/html/testdata/html5lib-tests/tree-construction/quirks01.dat b/html/testdata/html5lib-tests/tree-construction/quirks01.dat
new file mode 100644
index 0000000..bc58de5
--- /dev/null
+++ b/html/testdata/html5lib-tests/tree-construction/quirks01.dat
@@ -0,0 +1,53 @@
+#data
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd"><p><table>
+#errors
+(2,54): unknown-doctype
+(2,64): eof-in-table
+#document
+| <!DOCTYPE html "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|     <table>
+
+#data
+<!DOCTYPE html SYSTEM "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd"><p><table>
+#errors
+(1,83): unknown-doctype
+(1,93): eof-in-table
+#document
+| <!DOCTYPE html "" "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd">
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <table>
+
+#data
+<!DOCTYPE html PUBLIC "html"><p><table>
+#errors
+(1,30): unknown-doctype
+(1,39): eof-in-table
+#document
+| <!DOCTYPE html "html" "">
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <table>
+
+#data
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN"
+   "http://www.w3.org/TR/html4/strict.dtd"><p><table>
+#errors
+(2,43): unknown-doctype
+(2,53): eof-in-table
+#document
+| <!DOCTYPE html "-//W3C//DTD HTML 3.2//EN" "http://www.w3.org/TR/html4/strict.dtd">
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <table>
diff --git a/html/testdata/webkit/ruby.dat b/html/testdata/html5lib-tests/tree-construction/ruby.dat
similarity index 98%
rename from html/testdata/webkit/ruby.dat
rename to html/testdata/html5lib-tests/tree-construction/ruby.dat
index 696782f..f4e5e4e 100644
--- a/html/testdata/webkit/ruby.dat
+++ b/html/testdata/html5lib-tests/tree-construction/ruby.dat
@@ -203,6 +203,7 @@
 <html><ruby>a<rtc>b<span></ruby></html>
 #errors
 (1,6): expected-doctype-but-got-start-tag
+(1,32): unexpected-end-tag
 #document
 | <html>
 |   <head>
diff --git a/html/testdata/webkit/scriptdata01.dat b/html/testdata/html5lib-tests/tree-construction/scriptdata01.dat
similarity index 96%
rename from html/testdata/webkit/scriptdata01.dat
rename to html/testdata/html5lib-tests/tree-construction/scriptdata01.dat
index e570858..6abcb65 100644
--- a/html/testdata/webkit/scriptdata01.dat
+++ b/html/testdata/html5lib-tests/tree-construction/scriptdata01.dat
@@ -173,19 +173,6 @@
 |     "BAR"
 
 #data
-FOO<script>'<!-->'</script>BAR
-#errors
-(1,3): expected-doctype-but-got-chars
-#document
-| <html>
-|   <head>
-|   <body>
-|     "FOO"
-|     <script>
-|       "'<!-->'"
-|     "BAR"
-
-#data
 FOO<script>'<!-- potato'</script>BAR
 #errors
 (1,3): expected-doctype-but-got-chars
diff --git a/html/testdata/webkit/scripted/adoption01.dat b/html/testdata/html5lib-tests/tree-construction/scripted/adoption01.dat
similarity index 100%
rename from html/testdata/webkit/scripted/adoption01.dat
rename to html/testdata/html5lib-tests/tree-construction/scripted/adoption01.dat
diff --git a/html/testdata/webkit/scripted/ark.dat b/html/testdata/html5lib-tests/tree-construction/scripted/ark.dat
similarity index 100%
rename from html/testdata/webkit/scripted/ark.dat
rename to html/testdata/html5lib-tests/tree-construction/scripted/ark.dat
diff --git a/html/testdata/webkit/scripted/webkit01.dat b/html/testdata/html5lib-tests/tree-construction/scripted/webkit01.dat
similarity index 100%
rename from html/testdata/webkit/scripted/webkit01.dat
rename to html/testdata/html5lib-tests/tree-construction/scripted/webkit01.dat
diff --git a/html/testdata/html5lib-tests/tree-construction/search-element.dat b/html/testdata/html5lib-tests/tree-construction/search-element.dat
new file mode 100644
index 0000000..2866d7e
--- /dev/null
+++ b/html/testdata/html5lib-tests/tree-construction/search-element.dat
@@ -0,0 +1,46 @@
+#data
+<!doctype html><p>foo<search>bar<p>baz
+#errors
+(1,38): expected-closing-tag-but-got-eof
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       "foo"
+|     <search>
+|       "bar"
+|       <p>
+|         "baz"
+
+#data
+<!doctype html><search><p>foo</search>bar
+#errors
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     <search>
+|       <p>
+|         "foo"
+|     "bar"
+
+#data
+<!DOCTYPE html>xxx<svg><x><g><a><search><b>
+#errors
+ * (1,44) unexpected HTML-like start tag token in foreign content
+ * (1,44) unexpected end of file
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     "xxx"
+|     <svg svg>
+|       <svg x>
+|         <svg g>
+|           <svg a>
+|             <svg search>
+|     <b>
diff --git a/html/testdata/webkit/svg.dat b/html/testdata/html5lib-tests/tree-construction/svg.dat
similarity index 62%
rename from html/testdata/webkit/svg.dat
rename to html/testdata/html5lib-tests/tree-construction/svg.dat
index 8e9a2bb..a452e7a 100644
--- a/html/testdata/webkit/svg.dat
+++ b/html/testdata/html5lib-tests/tree-construction/svg.dat
@@ -1,6 +1,8 @@
 #data
 <svg><tr><td><title><tr>
 #errors
+(1:21) Unexpected <tr> tag
+(1:25) Unexpected EOF
 #document-fragment
 td
 #document
@@ -12,6 +14,9 @@
 #data
 <svg><tr><td><title><tr>
 #errors
+(1:1) Unexpected <svg> tag
+(1:21) Unexpected <tr> tag
+(1:25) Unexpected EOF
 #document-fragment
 tr
 #document
@@ -23,6 +28,9 @@
 #data
 <svg><thead><title><tbody>
 #errors
+(1:1) Unexpected <svg> tag
+(1:20) Unexpected <tbody> tag
+(1:27) Unexpected EOF
 #document-fragment
 thead
 #document
@@ -33,6 +41,9 @@
 #data
 <svg><tfoot><title><tbody>
 #errors
+(1:1) Unexpected <svg> tag
+(1:20) Unexpected <tbody> tag
+(1:27) Unexpected EOF
 #document-fragment
 tfoot
 #document
@@ -43,6 +54,9 @@
 #data
 <svg><tbody><title><tfoot>
 #errors
+(1:1) Unexpected <svg> tag
+(1:20) Unexpected <tfoot> tag
+(1:27) Unexpected EOF
 #document-fragment
 tbody
 #document
@@ -53,6 +67,9 @@
 #data
 <svg><tbody><title></table>
 #errors
+(1:1) Unexpected <svg> tag
+(1:20) Unexpected </table> tag
+(1:28) Unexpected EOF
 #document-fragment
 tbody
 #document
@@ -63,6 +80,9 @@
 #data
 <svg><thead><title></table>
 #errors
+(1:1) Unexpected <svg> tag
+(1:20) Unexpected </table> tag
+(1:28) Unexpected EOF
 #document-fragment
 tbody
 #document
@@ -73,6 +93,9 @@
 #data
 <svg><tfoot><title></table>
 #errors
+(1:1) Unexpected <svg> tag
+(1:20) Unexpected </table> tag
+(1:28) Unexpected EOF
 #document-fragment
 tbody
 #document
diff --git a/html/testdata/webkit/tables01.dat b/html/testdata/html5lib-tests/tree-construction/tables01.dat
similarity index 69%
rename from html/testdata/webkit/tables01.dat
rename to html/testdata/html5lib-tests/tree-construction/tables01.dat
index f0caaa3..4079888 100644
--- a/html/testdata/webkit/tables01.dat
+++ b/html/testdata/html5lib-tests/tree-construction/tables01.dat
@@ -100,8 +100,11 @@
 #data
 <table><select><option>3</select></table>
 #errors
-(1,7): expected-doctype-but-got-start-tag
-(1,15): unexpected-start-tag-implies-table-voodoo
+1:1: ERROR: Expected a doctype token
+1:8: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, table.
+1:16: ERROR: Start tag 'option' isn't allowed here. Currently open tags: html, body, table, select.
+1:24: ERROR: Character tokens aren't legal here
+1:25: ERROR: End tag 'select' isn't allowed here. Currently open tags: html, body, table, select, option.
 #document
 | <html>
 |   <head>
@@ -114,12 +117,11 @@
 #data
 <table><select><table></table></select></table>
 #errors
-(1,7): expected-doctype-but-got-start-tag
-(1,15): unexpected-start-tag-implies-table-voodoo
-(1,22): unexpected-table-element-start-tag-in-select-in-table
-(1,22): unexpected-start-tag-implies-end-tag
-(1,39): unexpected-end-tag
-(1,47): unexpected-end-tag
+1:1: ERROR: Expected a doctype token
+1:8: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, table.
+1:16: ERROR: Start tag 'table' isn't allowed here. Currently open tags: html, body, table, select.
+1:31: ERROR: End tag 'select' isn't allowed here. Currently open tags: html, body.
+1:40: ERROR: End tag 'table' isn't allowed here. Currently open tags: html, body.
 #document
 | <html>
 |   <head>
@@ -131,9 +133,8 @@
 #data
 <table><select></table>
 #errors
-(1,7): expected-doctype-but-got-start-tag
-(1,15): unexpected-start-tag-implies-table-voodoo
-(1,23): unexpected-table-element-end-tag-in-select-in-table
+1:1: ERROR: Expected a doctype token
+1:8: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, table.
 #document
 | <html>
 |   <head>
@@ -144,9 +145,10 @@
 #data
 <table><select><option>A<tr><td>B</td></tr></table>
 #errors
-(1,7): expected-doctype-but-got-start-tag
-(1,15): unexpected-start-tag-implies-table-voodoo
-(1,28): unexpected-table-element-start-tag-in-select-in-table
+1:1: ERROR: Expected a doctype token
+1:8: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, table.
+1:16: ERROR: Start tag 'option' isn't allowed here. Currently open tags: html, body, table, select.
+1:24: ERROR: Character tokens aren't legal here
 #document
 | <html>
 |   <head>
@@ -284,3 +286,38 @@
 |             <svg svg>
 |               <svg desc>
 |           <td>
+
+#data
+<div><table><svg><foreignObject><select><table><s>
+#errors
+1:1: Expected a doctype token
+1:13: 'svg' tag isn't allowed here. Currently open tags: html, body, div, table.
+1:33: 'select' tag isn't allowed here. Currently open tags: html, body, div, table, svg, foreignobject.
+1:41: 'table' tag isn't allowed here. Currently open tags: html, body, div, table, svg, foreignobject, select.
+1:48: 's' tag isn't allowed here. Currently open tags: html, body, div, table.
+1:51: Premature end of file. Currently open tags: html, body, div, table, s.
+#document
+| <html>
+|   <head>
+|   <body>
+|     <div>
+|       <svg svg>
+|         <svg foreignObject>
+|           <select>
+|       <table>
+|       <s>
+|       <table>
+
+#data
+<table>a<!doctype html>
+#errors
+(1,1): expected-doctype-but-got-start-tag
+(1,8): illegal-character-token
+(1,9): illegal-doctype
+(1,24): expected-closing-tag-but-got-eof
+#document
+| <html>
+|   <head>
+|   <body>
+|     "a"
+|     <table>
diff --git a/html/testdata/webkit/template.dat b/html/testdata/html5lib-tests/tree-construction/template.dat
similarity index 93%
rename from html/testdata/webkit/template.dat
rename to html/testdata/html5lib-tests/tree-construction/template.dat
index b38d4f5..45fb507 100644
--- a/html/testdata/webkit/template.dat
+++ b/html/testdata/html5lib-tests/tree-construction/template.dat
@@ -868,21 +868,6 @@
 |         <td>
 
 #data
-<body><template><template><tr></tr></template><td></td></template>
-#errors
-no doctype
-#document
-| <html>
-|   <head>
-|   <body>
-|     <template>
-|       content
-|         <template>
-|           content
-|             <tr>
-|         <td>
-
-#data
 <body><table><colgroup><template><col></col></template></colgroup></table></body>
 #errors
 no doctype
@@ -1089,7 +1074,11 @@
 <body><template><col>Hello
 #errors
 no doctype
-unexpected text
+(1,27): foster-parenting-character
+(1,27): foster-parenting-character
+(1,27): foster-parenting-character
+(1,27): foster-parenting-character
+(1,27): foster-parenting-character
 eof in template
 #document
 | <html>
@@ -1103,7 +1092,7 @@
 <body><template><i><menu>Foo</i>
 #errors
 no doctype
-mising /menu
+missing /menu
 eof in template
 #document
 | <html>
@@ -1569,6 +1558,19 @@
 |   <body>
 
 #data
+<html><head></head><template></template><head>
+#errors
+no doctype
+template-after-head
+head-after-head
+#document
+| <html>
+|   <head>
+|     <template>
+|       content
+|   <body>
+
+#data
 <!DOCTYPE HTML><dummy><table><template><table><template><table><script>
 #errors
 eof script
@@ -1593,6 +1595,11 @@
 #data
 <template><a><table><a>
 #errors
+(1,10): expected-doctype-but-got-start-tag
+(1,23): foster-parenting-start-tag
+(1,23): unexpected-start-tag
+(1,23): formatting-element-not-in-scope
+(1,24): eof-in-template
 #document
 | <html>
 |   <head>
@@ -1602,3 +1609,65 @@
 |           <a>
 |           <table>
 |   <body>
+
+#data
+<template><form><input name="q"></form><div>second</div></template>
+#errors
+#document-fragment
+template
+#document
+| <template>
+|   content
+|     <form>
+|       <input>
+|         name="q"
+|     <div>
+|       "second"
+
+#data
+<!DOCTYPE HTML><template><tr><td>cell</td></tr></template>
+#errors
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|     <template>
+|       content
+|         <tr>
+|           <td>
+|             "cell"
+|   <body>
+
+#data
+<!DOCTYPE HTML><template> <tr> <td>cell</td> </tr> </template>
+#errors
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|     <template>
+|       content
+|         " "
+|         <tr>
+|           " "
+|           <td>
+|             "cell"
+|           " "
+|         " "
+|   <body>
+
+#data
+<!DOCTYPE HTML><template><tr><td>cell</td></tr>a</template>
+#errors
+(1,59): foster-parenting-character
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|     <template>
+|       content
+|         <tr>
+|           <td>
+|             "cell"
+|         "a"
+|   <body>
diff --git a/html/testdata/webkit/tests1.dat b/html/testdata/html5lib-tests/tree-construction/tests1.dat
similarity index 97%
rename from html/testdata/webkit/tests1.dat
rename to html/testdata/html5lib-tests/tree-construction/tests1.dat
index 1c36c1b..15d496e 100644
--- a/html/testdata/webkit/tests1.dat
+++ b/html/testdata/html5lib-tests/tree-construction/tests1.dat
@@ -355,19 +355,20 @@
 #data
 <select><b><option><select><option></b></select>X
 #errors
-(1,8): expected-doctype-but-got-start-tag
-(1,11): unexpected-start-tag-in-select
-(1,27): unexpected-select-in-select
-(1,39): unexpected-end-tag
-(1,48): unexpected-end-tag
+1:1: ERROR: Expected a doctype token
+1:20: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, select, b, option.
+1:36: ERROR: End tag 'b' isn't allowed here. Currently open tags: html, body, b, select, option.
+1:50: ERROR: Premature end of file. Currently open tags: html, body, b.
 #document
 | <html>
 |   <head>
 |   <body>
 |     <select>
+|       <b>
+|         <option>
+|     <b>
 |       <option>
-|     <option>
-|       "X"
+|     "X"
 
 #data
 <a><table><td><a><table></table><a></tr><a></table><b>X</b>C<a>Y
@@ -425,7 +426,6 @@
 #data
 <!-----><font><div>hello<table>excite!<b>me!<th><i>please!</tr><!--X-->
 #errors
-(1,7): unexpected-dash-after-double-dash-in-comment
 (1,14): expected-doctype-but-got-start-tag
 (1,41): unexpected-start-tag-implies-table-voodoo
 (1,48): foster-parenting-character-in-table
@@ -1435,24 +1435,6 @@
 |     <p>
 
 #data
-<b><table><td><i></table>
-#errors
-(1,3): expected-doctype-but-got-start-tag
-(1,14): unexpected-cell-in-table-body
-(1,25): unexpected-cell-end-tag
-(1,25): expected-closing-tag-but-got-eof
-#document
-| <html>
-|   <head>
-|   <body>
-|     <b>
-|       <table>
-|         <tbody>
-|           <tr>
-|             <td>
-|               <i>
-
-#data
 <b><table><td></b><i></table>
 #errors
 (1,3): expected-doctype-but-got-start-tag
@@ -1549,33 +1531,21 @@
 |     <p>
 
 #data
-<p><hr></p>
-#errors
-(1,3): expected-doctype-but-got-start-tag
-(1,11): unexpected-end-tag
-#document
-| <html>
-|   <head>
-|   <body>
-|     <p>
-|     <hr>
-|     <p>
-
-#data
 <select><b><option><select><option></b></select>
 #errors
-(1,8): expected-doctype-but-got-start-tag
-(1,11): unexpected-start-tag-in-select
-(1,27): unexpected-select-in-select
-(1,39): unexpected-end-tag
-(1,48): unexpected-end-tag
+1:1: ERROR: Expected a doctype token
+1:20: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, select, b, option.
+1:36: ERROR: End tag 'b' isn't allowed here. Currently open tags: html, body, b, select, option.
+1:49: ERROR: Premature end of file. Currently open tags: html, body, b.
 #document
 | <html>
 |   <head>
 |   <body>
 |     <select>
+|       <b>
+|         <option>
+|     <b>
 |       <option>
-|     <option>
 
 #data
 <html><head><title></title><body></body></html>
diff --git a/html/testdata/webkit/tests10.dat b/html/testdata/html5lib-tests/tree-construction/tests10.dat
similarity index 93%
rename from html/testdata/webkit/tests10.dat
rename to html/testdata/html5lib-tests/tree-construction/tests10.dat
index f84e2d5..3b311e4 100644
--- a/html/testdata/webkit/tests10.dat
+++ b/html/testdata/html5lib-tests/tree-construction/tests10.dat
@@ -35,20 +35,17 @@
 #data
 <!DOCTYPE html><body><select><svg></svg></select>
 #errors
-(1,34) unexpected-start-tag-in-select
-(1,40) unexpected-end-tag-in-select
 #document
 | <!DOCTYPE html>
 | <html>
 |   <head>
 |   <body>
 |     <select>
+|       <svg svg>
 
 #data
 <!DOCTYPE html><body><select><option><svg></svg></option></select>
 #errors
-(1,42) unexpected-start-tag-in-select
-(1,48) unexpected-end-tag-in-select
 #document
 | <!DOCTYPE html>
 | <html>
@@ -56,6 +53,7 @@
 |   <body>
 |     <select>
 |       <option>
+|         <svg svg>
 
 #data
 <!DOCTYPE html><body><table><svg></svg></table>
@@ -261,13 +259,8 @@
 #data
 <!DOCTYPE html><body><table><tr><td><select><svg><g>foo</g><g>bar</g><p>baz</table><p>quux
 #errors
-(1,49) unexpected-start-tag-in-select
-(1,52) unexpected-start-tag-in-select
-(1,59) unexpected-end-tag-in-select
-(1,62) unexpected-start-tag-in-select
-(1,69) unexpected-end-tag-in-select
-(1,72) unexpected-start-tag-in-select
-(1,83) unexpected-table-element-end-tag-in-select-in-table
+1:70: ERROR: Start tag 'p' isn't allowed here. Currently open tags: html, body, table, tbody, tr, td, select, svg.
+1:76: ERROR: End tag 'table' isn't allowed here. Currently open tags: html, body, table, tbody, tr, td, select.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -278,28 +271,39 @@
 |         <tr>
 |           <td>
 |             <select>
-|               "foobarbaz"
+|               <svg svg>
+|                 <svg g>
+|                   "foo"
+|                 <svg g>
+|                   "bar"
+|               <p>
+|                 "baz"
 |     <p>
 |       "quux"
 
 #data
 <!DOCTYPE html><body><table><select><svg><g>foo</g><g>bar</g><p>baz</table><p>quux
 #errors
-(1,36) unexpected-start-tag-implies-table-voodoo
-(1,41) unexpected-start-tag-in-select
-(1,44) unexpected-start-tag-in-select
-(1,51) unexpected-end-tag-in-select
-(1,54) unexpected-start-tag-in-select
-(1,61) unexpected-end-tag-in-select
-(1,64) unexpected-start-tag-in-select
-(1,75) unexpected-table-element-end-tag-in-select-in-table
+1:29: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, table.
+1:37: ERROR: Start tag 'svg' isn't allowed here. Currently open tags: html, body, table, select.
+1:62: ERROR: Start tag 'p' isn't allowed here. Currently open tags: html, body, table, select, svg.
+1:62: ERROR: Start tag 'p' isn't allowed here. Currently open tags: html, body, table, select.
+1:65: ERROR: Character tokens aren't legal here
+1:66: ERROR: Character tokens aren't legal here
+1:67: ERROR: Character tokens aren't legal here
 #document
 | <!DOCTYPE html>
 | <html>
 |   <head>
 |   <body>
 |     <select>
-|       "foobarbaz"
+|       <svg svg>
+|         <svg g>
+|           "foo"
+|         <svg g>
+|           "bar"
+|       <p>
+|         "baz"
 |     <table>
 |     <p>
 |       "quux"
diff --git a/html/testdata/webkit/tests11.dat b/html/testdata/html5lib-tests/tree-construction/tests11.dat
similarity index 100%
rename from html/testdata/webkit/tests11.dat
rename to html/testdata/html5lib-tests/tree-construction/tests11.dat
diff --git a/html/testdata/webkit/tests12.dat b/html/testdata/html5lib-tests/tree-construction/tests12.dat
similarity index 100%
rename from html/testdata/webkit/tests12.dat
rename to html/testdata/html5lib-tests/tree-construction/tests12.dat
diff --git a/html/testdata/webkit/tests14.dat b/html/testdata/html5lib-tests/tree-construction/tests14.dat
similarity index 100%
rename from html/testdata/webkit/tests14.dat
rename to html/testdata/html5lib-tests/tree-construction/tests14.dat
diff --git a/html/testdata/webkit/tests15.dat b/html/testdata/html5lib-tests/tree-construction/tests15.dat
similarity index 100%
rename from html/testdata/webkit/tests15.dat
rename to html/testdata/html5lib-tests/tree-construction/tests15.dat
diff --git a/html/testdata/webkit/tests16.dat b/html/testdata/html5lib-tests/tree-construction/tests16.dat
similarity index 99%
rename from html/testdata/webkit/tests16.dat
rename to html/testdata/html5lib-tests/tree-construction/tests16.dat
index cea7340..05f34c1 100644
--- a/html/testdata/webkit/tests16.dat
+++ b/html/testdata/html5lib-tests/tree-construction/tests16.dat
@@ -221,7 +221,6 @@
 <!doctype html><script><!
 #errors
 (1,25): expected-script-data-but-got-eof
-(1,25): expected-named-closing-tag-but-got-eof
 #document
 | <!DOCTYPE html>
 | <html>
@@ -1525,7 +1524,6 @@
 #errors
 (1,8): expected-doctype-but-got-start-tag
 (1,10): expected-script-data-but-got-eof
-(1,10): expected-named-closing-tag-but-got-eof
 #document
 | <html>
 |   <head>
diff --git a/html/testdata/webkit/tests17.dat b/html/testdata/html5lib-tests/tree-construction/tests17.dat
similarity index 90%
rename from html/testdata/webkit/tests17.dat
rename to html/testdata/html5lib-tests/tree-construction/tests17.dat
index e49bcf0..1c472d1 100644
--- a/html/testdata/webkit/tests17.dat
+++ b/html/testdata/html5lib-tests/tree-construction/tests17.dat
@@ -1,9 +1,8 @@
 #data
 <!doctype html><table><tbody><select><tr>
 #errors
-(1,37): unexpected-start-tag-implies-table-voodoo
-(1,41): unexpected-table-element-start-tag-in-select-in-table
-(1,41): eof-in-table
+(1,30): unexpected-start-tag
+(1,42): premature-eof
 #document
 | <!DOCTYPE html>
 | <html>
@@ -17,9 +16,8 @@
 #data
 <!doctype html><table><tr><select><td>
 #errors
-(1,34): unexpected-start-tag-implies-table-voodoo
-(1,38): unexpected-table-element-start-tag-in-select-in-table
-(1,38): expected-closing-tag-but-got-eof
+(1,27): unexpected-start-tag
+(1,39): premature-eof
 #document
 | <!DOCTYPE html>
 | <html>
diff --git a/html/testdata/webkit/tests18.dat b/html/testdata/html5lib-tests/tree-construction/tests18.dat
similarity index 71%
rename from html/testdata/webkit/tests18.dat
rename to html/testdata/html5lib-tests/tree-construction/tests18.dat
index 05363b3..a3bb69a 100644
--- a/html/testdata/webkit/tests18.dat
+++ b/html/testdata/html5lib-tests/tree-construction/tests18.dat
@@ -3,7 +3,6 @@
 #errors
 11: Start tag seen without seeing a doctype first. Expected “<!DOCTYPE html>”.
 23: End of file seen and there were open elements.
-11: Unclosed element “plaintext”.
 #document
 | <html>
 |   <head>
@@ -27,7 +26,6 @@
 <!doctype html><html><plaintext></plaintext>
 #errors
 44: End of file seen and there were open elements.
-32: Unclosed element “plaintext”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -40,7 +38,6 @@
 <!doctype html><head><plaintext></plaintext>
 #errors
 44: End of file seen and there were open elements.
-32: Unclosed element “plaintext”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -54,7 +51,6 @@
 #errors
 42: Bad start tag in “plaintext” in “head”.
 54: End of file seen and there were open elements.
-42: Unclosed element “plaintext”.
 #script-off
 #document
 | <!DOCTYPE html>
@@ -69,7 +65,6 @@
 <!doctype html></head><plaintext></plaintext>
 #errors
 45: End of file seen and there were open elements.
-33: Unclosed element “plaintext”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -82,7 +77,6 @@
 <!doctype html><body><plaintext></plaintext>
 #errors
 44: End of file seen and there were open elements.
-32: Unclosed element “plaintext”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -95,8 +89,19 @@
 <!doctype html><table><plaintext></plaintext>
 #errors
 (1,33): foster-parenting-start-tag
-(1,45): foster-parenting-character
-(1,45): eof-in-table
+(1,46): foster-parenting-character
+(1,46): foster-parenting-character
+(1,46): foster-parenting-character
+(1,46): foster-parenting-character
+(1,46): foster-parenting-character
+(1,46): foster-parenting-character
+(1,46): foster-parenting-character
+(1,46): foster-parenting-character
+(1,46): foster-parenting-character
+(1,46): foster-parenting-character
+(1,46): foster-parenting-character
+(1,46): foster-parenting-character
+(1,46): eof-in-table
 #document
 | <!DOCTYPE html>
 | <html>
@@ -110,8 +115,19 @@
 <!doctype html><table><tbody><plaintext></plaintext>
 #errors
 (1,40): foster-parenting-start-tag
-(1,41): foster-parenting-character
-(1,52): eof-in-table
+(1,53): foster-parenting-character
+(1,53): foster-parenting-character
+(1,53): foster-parenting-character
+(1,53): foster-parenting-character
+(1,53): foster-parenting-character
+(1,53): foster-parenting-character
+(1,53): foster-parenting-character
+(1,53): foster-parenting-character
+(1,53): foster-parenting-character
+(1,53): foster-parenting-character
+(1,53): foster-parenting-character
+(1,53): foster-parenting-character
+(1,53): eof-in-table
 #document
 | <!DOCTYPE html>
 | <html>
@@ -126,8 +142,19 @@
 <!doctype html><table><tbody><tr><plaintext></plaintext>
 #errors
 (1,44): foster-parenting-start-tag
-(1,56): foster-parenting-character
-(1,56): eof-in-table
+(1,57): foster-parenting-character
+(1,57): foster-parenting-character
+(1,57): foster-parenting-character
+(1,57): foster-parenting-character
+(1,57): foster-parenting-character
+(1,57): foster-parenting-character
+(1,57): foster-parenting-character
+(1,57): foster-parenting-character
+(1,57): foster-parenting-character
+(1,57): foster-parenting-character
+(1,57): foster-parenting-character
+(1,57): foster-parenting-character
+(1,57): eof-in-table
 #document
 | <!DOCTYPE html>
 | <html>
@@ -173,11 +200,20 @@
 #data
 <!doctype html><table><colgroup><plaintext></plaintext>
 #errors
-43: Start tag “plaintext” seen in “table”.
-55: Misplaced non-space characters inside a table.
+(1,43): foster-parenting-start-tag
+(1,56): foster-parenting-character
+(1,56): foster-parenting-character
+(1,56): foster-parenting-character
+(1,56): foster-parenting-character
+(1,56): foster-parenting-character
+(1,56): foster-parenting-character
+(1,56): foster-parenting-character
+(1,56): foster-parenting-character
+(1,56): foster-parenting-character
+(1,56): foster-parenting-character
+(1,56): foster-parenting-character
+(1,56): foster-parenting-character
 55: End of file seen and there were open elements.
-43: Unclosed element “plaintext”.
-22: Unclosed element “table”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -191,44 +227,47 @@
 #data
 <!doctype html><select><plaintext></plaintext>X
 #errors
-34: Stray start tag “plaintext”.
-46: Stray end tag “plaintext”.
-47: End of file seen and there were open elements.
-23: Unclosed element “select”.
+1:48: ERROR: Premature end of file. Currently open tags: html, body, select, plaintext.
 #document
 | <!DOCTYPE html>
 | <html>
 |   <head>
 |   <body>
 |     <select>
-|       "X"
+|       <plaintext>
+|         "</plaintext>X"
 
 #data
 <!doctype html><table><select><plaintext>a<caption>b
 #errors
-30: Start tag “select” seen in “table”.
-41: Stray start tag “plaintext”.
-51: “caption” start tag with “select” open.
-52: End of file seen and there were open elements.
-51: Unclosed element “caption”.
-22: Unclosed element “table”.
+1:23: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, table.
+1:31: ERROR: Start tag 'plaintext' isn't allowed here. Currently open tags: html, body, table, select.
+1:42: ERROR: Character tokens aren't legal here
+1:43: ERROR: Character tokens aren't legal here
+1:44: ERROR: Character tokens aren't legal here
+1:45: ERROR: Character tokens aren't legal here
+1:46: ERROR: Character tokens aren't legal here
+1:47: ERROR: Character tokens aren't legal here
+1:48: ERROR: Character tokens aren't legal here
+1:49: ERROR: Character tokens aren't legal here
+1:50: ERROR: Character tokens aren't legal here
+1:51: ERROR: Character tokens aren't legal here
+1:52: ERROR: Character tokens aren't legal here
+1:53: ERROR: Premature end of file. Currently open tags: html, body, table, select, plaintext.
 #document
 | <!DOCTYPE html>
 | <html>
 |   <head>
 |   <body>
 |     <select>
-|       "a"
+|       <plaintext>
+|         "a<caption>b"
 |     <table>
-|       <caption>
-|         "b"
 
 #data
 <!doctype html><template><plaintext>a</template>b
 #errors
 49: End of file seen and there were open elements.
-36: Unclosed element “plaintext”.
-25: Unclosed element “template”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -244,7 +283,6 @@
 #errors
 39: Stray start tag “plaintext”.
 51: End of file seen and there were open elements.
-39: Unclosed element “plaintext”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -259,7 +297,6 @@
 36: Stray start tag “plaintext”.
 48: Stray end tag “plaintext”.
 48: End of file seen and there were open elements.
-25: Unclosed element “frameset”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -282,7 +319,6 @@
 #errors
 46: Stray start tag “plaintext”.
 58: End of file seen and there were open elements.
-46: Unclosed element “plaintext”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -306,7 +342,6 @@
 <!doctype html><svg><plaintext>a</plaintext>b
 #errors
 45: End of file seen and there were open elements.
-20: Unclosed element “svg”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -321,9 +356,6 @@
 <!doctype html><svg><title><plaintext>a</plaintext>b
 #errors
 52: End of file seen and there were open elements.
-38: Unclosed element “plaintext”.
-27: Unclosed element “title”.
-20: Unclosed element “svg”.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -422,8 +454,11 @@
 #data
 <!doctype html><table><select><script></style></script>abc
 #errors
-(1,30): unexpected-start-tag-implies-table-voodoo
-(1,58): eof-in-select
+1:23: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, table.
+1:56: ERROR: Character tokens aren't legal here
+1:57: ERROR: Character tokens aren't legal here
+1:58: ERROR: Character tokens aren't legal here
+1:59: ERROR: Premature end of file. Currently open tags: html, body, table, select.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -438,8 +473,11 @@
 #data
 <!doctype html><table><tr><select><script></style></script>abc
 #errors
-(1,34): unexpected-start-tag-implies-table-voodoo
-(1,62): eof-in-select
+1:27: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, table, tbody, tr.
+1:60: ERROR: Character tokens aren't legal here
+1:61: ERROR: Character tokens aren't legal here
+1:62: ERROR: Character tokens aren't legal here
+1:63: ERROR: Premature end of file. Currently open tags: html, body, table, tbody, tr, select.
 #document
 | <!DOCTYPE html>
 | <html>
diff --git a/html/testdata/webkit/tests19.dat b/html/testdata/html5lib-tests/tree-construction/tests19.dat
similarity index 94%
rename from html/testdata/webkit/tests19.dat
rename to html/testdata/html5lib-tests/tree-construction/tests19.dat
index a189777..20cdeab 100644
--- a/html/testdata/webkit/tests19.dat
+++ b/html/testdata/html5lib-tests/tree-construction/tests19.dat
@@ -388,19 +388,6 @@
 |       <option>
 
 #data
-<!doctype html><select><option></optgroup>
-#errors
-(1,42): unexpected-end-tag-in-select
-(1,42): eof-in-select
-#document
-| <!DOCTYPE html>
-| <html>
-|   <head>
-|   <body>
-|     <select>
-|       <option>
-
-#data
 <!doctype html><dd><optgroup><dd>
 #errors
 #document
@@ -1015,7 +1002,6 @@
 <!doctype html><p><math></p>a
 #errors
 (1,28): unexpected-end-tag
-(1,28): unexpected-end-tag
 #document
 | <!DOCTYPE html>
 | <html>
@@ -1237,48 +1223,6 @@
 |     <table>
 
 #data
-<!doctype html><table><i>a<b>b<div>c<a>d</i>e</b>f
-#errors
-(1,25): foster-parenting-start-tag
-(1,26): foster-parenting-character
-(1,29): foster-parenting-start-tag
-(1,30): foster-parenting-character
-(1,35): foster-parenting-start-tag
-(1,36): foster-parenting-character
-(1,39): foster-parenting-start-tag
-(1,40): foster-parenting-character
-(1,44): foster-parenting-end-tag
-(1,44): adoption-agency-1.3
-(1,44): adoption-agency-1.3
-(1,45): foster-parenting-character
-(1,49): foster-parenting-end-tag
-(1,44): adoption-agency-1.3
-(1,44): adoption-agency-1.3
-(1,50): foster-parenting-character
-(1,50): eof-in-table
-#document
-| <!DOCTYPE html>
-| <html>
-|   <head>
-|   <body>
-|     <i>
-|       "a"
-|       <b>
-|         "b"
-|     <b>
-|     <div>
-|       <b>
-|         <i>
-|           "c"
-|           <a>
-|             "d"
-|         <a>
-|           "e"
-|       <a>
-|         "f"
-|     <table>
-
-#data
 <!doctype html><table><i>a<div>b<tr>c<b>d</i>e
 #errors
 (1,25): foster-parenting-start-tag
diff --git a/html/testdata/webkit/tests2.dat b/html/testdata/html5lib-tests/tree-construction/tests2.dat
similarity index 97%
rename from html/testdata/webkit/tests2.dat
rename to html/testdata/html5lib-tests/tree-construction/tests2.dat
index b44fec4..9232edd 100644
--- a/html/testdata/webkit/tests2.dat
+++ b/html/testdata/html5lib-tests/tree-construction/tests2.dat
@@ -500,7 +500,7 @@
 #data
 <!DOCTYPE html><select><optgroup><option></optgroup><option><select><option>
 #errors
-(1,68): unexpected-select-in-select
+1:61: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, select, option.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -585,6 +585,16 @@
 |   <body>
 
 #data
+<!DOCTYPE html> <!DOCTYPE html>
+#errors
+Line: 1 Col: 31 Unexpected DOCTYPE. Ignored.
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+
+#data
 test
 test
 #errors
diff --git a/html/testdata/webkit/tests20.dat b/html/testdata/html5lib-tests/tree-construction/tests20.dat
similarity index 70%
rename from html/testdata/webkit/tests20.dat
rename to html/testdata/html5lib-tests/tree-construction/tests20.dat
index afdae74..80c57d1 100644
--- a/html/testdata/webkit/tests20.dat
+++ b/html/testdata/html5lib-tests/tree-construction/tests20.dat
@@ -26,6 +26,32 @@
 |         <address>
 
 #data
+<!doctype html><p><button><article>
+#errors
+(1,36): expected-closing-tag-but-got-eof
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <button>
+|         <article>
+
+#data
+<!doctype html><p><button><aside>
+#errors
+(1,34): expected-closing-tag-but-got-eof
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <button>
+|         <aside>
+
+#data
 <!doctype html><p><button><blockquote>
 #errors
 (1,38): expected-closing-tag-but-got-eof
@@ -39,6 +65,175 @@
 |         <blockquote>
 
 #data
+<!doctype html><p><button><center>
+#errors
+(1,35): expected-closing-tag-but-got-eof
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <button>
+|         <center>
+
+#data
+<!doctype html><p><button><details>
+#errors
+(1,36): expected-closing-tag-but-got-eof
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <button>
+|         <details>
+
+#data
+<!doctype html><p><button><dialog>
+#errors
+(1,35): expected-closing-tag-but-got-eof
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <button>
+|         <dialog>
+
+#data
+<!doctype html><p><button><dir>
+#errors
+(1,32): expected-closing-tag-but-got-eof
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <button>
+|         <dir>
+
+#data
+<!doctype html><p><button><div>
+#errors
+(1,32): expected-closing-tag-but-got-eof
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <button>
+|         <div>
+
+#data
+<!doctype html><p><button><dl>
+#errors
+(1,31): expected-closing-tag-but-got-eof
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <button>
+|         <dl>
+
+#data
+<!doctype html><p><button><fieldset>
+#errors
+(1,37): expected-closing-tag-but-got-eof
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <button>
+|         <fieldset>
+
+#data
+<!doctype html><p><button><figcaption>
+#errors
+(1,39): expected-closing-tag-but-got-eof
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <button>
+|         <figcaption>
+
+#data
+<!doctype html><p><button><figure>
+#errors
+(1,35): expected-closing-tag-but-got-eof
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <button>
+|         <figure>
+
+#data
+<!doctype html><p><button><footer>
+#errors
+(1,35): expected-closing-tag-but-got-eof
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <button>
+|         <footer>
+
+#data
+<!doctype html><p><button><header>
+#errors
+(1,35): expected-closing-tag-but-got-eof
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <button>
+|         <header>
+
+#data
+<!doctype html><p><button><hgroup>
+#errors
+(1,35): expected-closing-tag-but-got-eof
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <button>
+|         <hgroup>
+
+#data
+<!doctype html><p><button><main>
+#errors
+(1,33): expected-closing-tag-but-got-eof
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <button>
+|         <main>
+
+#data
 <!doctype html><p><button><menu>
 #errors
 (1,32): expected-closing-tag-but-got-eof
@@ -52,6 +247,32 @@
 |         <menu>
 
 #data
+<!doctype html><p><button><nav>
+#errors
+(1,32): expected-closing-tag-but-got-eof
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <button>
+|         <nav>
+
+#data
+<!doctype html><p><button><ol>
+#errors
+(1,31): expected-closing-tag-but-got-eof
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <button>
+|         <ol>
+
+#data
 <!doctype html><p><button><p>
 #errors
 (1,29): expected-closing-tag-but-got-eof
@@ -65,6 +286,45 @@
 |         <p>
 
 #data
+<!doctype html><p><button><search>
+#errors
+(1,35): expected-closing-tag-but-got-eof
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <button>
+|         <search>
+
+#data
+<!doctype html><p><button><section>
+#errors
+(1,36): expected-closing-tag-but-got-eof
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <button>
+|         <section>
+
+#data
+<!doctype html><p><button><summary>
+#errors
+(1,36): expected-closing-tag-but-got-eof
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       <button>
+|         <summary>
+
+#data
 <!doctype html><p><button><ul>
 #errors
 (1,30): expected-closing-tag-but-got-eof
@@ -249,17 +509,16 @@
 |         <p>
 
 #data
-<!doctype html><address><button></address>a
+<!doctype html><button><p></button>x
 #errors
-(1,42): end-tag-too-early
 #document
 | <!DOCTYPE html>
 | <html>
 |   <head>
 |   <body>
-|     <address>
-|       <button>
-|     "a"
+|     <button>
+|       <p>
+|     "x"
 
 #data
 <!doctype html><address><button></address>a
@@ -557,6 +816,7 @@
 <math><annotation-xml></svg>x
 #errors
 (1,6): expected-doctype-but-got-start-tag
+(1,28): unexpected-end-tag-in-math
 (1,28): unexpected-end-tag
 (1,29): expected-closing-tag-but-got-eof
 #document
diff --git a/html/testdata/webkit/tests21.dat b/html/testdata/html5lib-tests/tree-construction/tests21.dat
similarity index 92%
rename from html/testdata/webkit/tests21.dat
rename to html/testdata/html5lib-tests/tree-construction/tests21.dat
index 1e2af7c..a926b13 100644
--- a/html/testdata/webkit/tests21.dat
+++ b/html/testdata/html5lib-tests/tree-construction/tests21.dat
@@ -41,20 +41,7 @@
 <svg><![CDATA[foo
 #errors
 (1,5): expected-doctype-but-got-start-tag
-(1,17): expected-closing-tag-but-got-eof
-#new-errors
 (1:18) eof-in-cdata
-#document
-| <html>
-|   <head>
-|   <body>
-|     <svg svg>
-|       "foo"
-
-#data
-<svg><![CDATA[foo
-#errors
-(1,5): expected-doctype-but-got-start-tag
 (1,17): expected-closing-tag-but-got-eof
 #new-errors
 (1:18) eof-in-cdata
@@ -69,6 +56,7 @@
 <svg><![CDATA[
 #errors
 (1,5): expected-doctype-but-got-start-tag
+(1:15) eof-in-cdata
 (1,14): expected-closing-tag-but-got-eof
 #new-errors
 (1:15) eof-in-cdata
@@ -102,21 +90,10 @@
 |       "]] >"
 
 #data
-<svg><![CDATA[]] >]]>
-#errors
-(1,5): expected-doctype-but-got-start-tag
-(1,21): expected-closing-tag-but-got-eof
-#document
-| <html>
-|   <head>
-|   <body>
-|     <svg svg>
-|       "]] >"
-
-#data
 <svg><![CDATA[]]
 #errors
 (1,5): expected-doctype-but-got-start-tag
+(1:17) eof-in-cdata
 (1,16): expected-closing-tag-but-got-eof
 #new-errors
 (1:17) eof-in-cdata
@@ -131,6 +108,7 @@
 <svg><![CDATA[]
 #errors
 (1,5): expected-doctype-but-got-start-tag
+(1:16) eof-in-cdata
 (1,15): expected-closing-tag-but-got-eof
 #new-errors
 (1:16) eof-in-cdata
@@ -145,6 +123,7 @@
 <svg><![CDATA[]>a
 #errors
 (1,5): expected-doctype-but-got-start-tag
+(1:16) eof-in-cdata
 (1,17): expected-closing-tag-but-got-eof
 #new-errors
 (1:18) eof-in-cdata
@@ -236,6 +215,7 @@
 <svg><![CDATA[<svg>a
 #errors
 (1,5): expected-doctype-but-got-start-tag
+(1:21) eof-in-cdata
 (1,20): expected-closing-tag-but-got-eof
 #new-errors
 (1:21) eof-in-cdata
@@ -250,6 +230,7 @@
 <svg><![CDATA[</svg>a
 #errors
 (1,5): expected-doctype-but-got-start-tag
+(1:22) eof-in-cdata
 (1,21): expected-closing-tag-but-got-eof
 #new-errors
 (1:22) eof-in-cdata
diff --git a/html/testdata/webkit/tests22.dat b/html/testdata/html5lib-tests/tree-construction/tests22.dat
similarity index 100%
rename from html/testdata/webkit/tests22.dat
rename to html/testdata/html5lib-tests/tree-construction/tests22.dat
diff --git a/html/testdata/webkit/tests23.dat b/html/testdata/html5lib-tests/tree-construction/tests23.dat
similarity index 100%
rename from html/testdata/webkit/tests23.dat
rename to html/testdata/html5lib-tests/tree-construction/tests23.dat
diff --git a/html/testdata/webkit/tests24.dat b/html/testdata/html5lib-tests/tree-construction/tests24.dat
similarity index 100%
rename from html/testdata/webkit/tests24.dat
rename to html/testdata/html5lib-tests/tree-construction/tests24.dat
diff --git a/html/testdata/webkit/tests25.dat b/html/testdata/html5lib-tests/tree-construction/tests25.dat
similarity index 100%
rename from html/testdata/webkit/tests25.dat
rename to html/testdata/html5lib-tests/tree-construction/tests25.dat
diff --git a/html/testdata/webkit/tests26.dat b/html/testdata/html5lib-tests/tree-construction/tests26.dat
similarity index 87%
rename from html/testdata/webkit/tests26.dat
rename to html/testdata/html5lib-tests/tree-construction/tests26.dat
index de453b9..1ba2be2 100644
--- a/html/testdata/webkit/tests26.dat
+++ b/html/testdata/html5lib-tests/tree-construction/tests26.dat
@@ -391,3 +391,63 @@
 |     <button>
 |       <p>
 |     <button>
+
+#data
+<svg></p><foo>
+#errors
+(1:1) Missing doctype
+9: HTML end tag “p” in a foreign namespace context.
+(1:6) Unexpected </p> from in body insertion mode
+(1:16) Unexpected EOF
+#document
+| <html>
+|   <head>
+|   <body>
+|     <svg svg>
+|     <p>
+|     <foo>
+
+#data
+<svg></br><foo>
+#errors
+(1:1) Missing doctype
+10: HTML end tag “br” in a foreign namespace context.
+(1:6) Unexpected </br> from in body insertion mode
+(1:16) Unexpected EOF
+#document
+| <html>
+|   <head>
+|   <body>
+|     <svg svg>
+|     <br>
+|     <foo>
+
+#data
+<math></p><foo>
+#errors
+(1:1) Missing doctype
+10: HTML end tag “p” in a foreign namespace context.
+(1:7) Unexpected </p> from in body insertion mode
+(1:16) Unexpected EOF
+#document
+| <html>
+|   <head>
+|   <body>
+|     <math math>
+|     <p>
+|     <foo>
+
+#data
+<math></br><foo>
+#errors
+(1:1) Missing doctype
+11: HTML end tag “br” in a foreign namespace context.
+(1:7) Unexpected </br> from in body insertion mode
+(1:17) Unexpected EOF
+#document
+| <html>
+|   <head>
+|   <body>
+|     <math math>
+|     <br>
+|     <foo>
diff --git a/html/testdata/webkit/tests3.dat b/html/testdata/html5lib-tests/tree-construction/tests3.dat
similarity index 100%
rename from html/testdata/webkit/tests3.dat
rename to html/testdata/html5lib-tests/tree-construction/tests3.dat
diff --git a/html/testdata/webkit/tests4.dat b/html/testdata/html5lib-tests/tree-construction/tests4.dat
similarity index 79%
rename from html/testdata/webkit/tests4.dat
rename to html/testdata/html5lib-tests/tree-construction/tests4.dat
index 0a6174c..4f0cf70 100644
--- a/html/testdata/webkit/tests4.dat
+++ b/html/testdata/html5lib-tests/tree-construction/tests4.dat
@@ -56,3 +56,19 @@
 #document
 | <title>
 |   "setting head's innerHTML"
+
+#data
+direct <title> content
+#errors
+#document-fragment
+title
+#document
+| "direct <title> content"
+
+#data
+<!-- inside </script> -->
+#errors
+#document-fragment
+script
+#document
+| "<!-- inside </script> -->"
diff --git a/html/testdata/webkit/tests5.dat b/html/testdata/html5lib-tests/tree-construction/tests5.dat
similarity index 100%
rename from html/testdata/webkit/tests5.dat
rename to html/testdata/html5lib-tests/tree-construction/tests5.dat
diff --git a/html/testdata/webkit/tests6.dat b/html/testdata/html5lib-tests/tree-construction/tests6.dat
similarity index 99%
rename from html/testdata/webkit/tests6.dat
rename to html/testdata/html5lib-tests/tree-construction/tests6.dat
index f399123..8c36dd3 100644
--- a/html/testdata/webkit/tests6.dat
+++ b/html/testdata/html5lib-tests/tree-construction/tests6.dat
@@ -48,7 +48,6 @@
 #data
 <!doctype>
 #errors
-(1,9): need-space-after-doctype
 (1,10): expected-doctype-name-but-got-right-bracket
 (1,10): unknown-doctype
 #new-errors
@@ -604,6 +603,7 @@
 #data
 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"><html></html>
 #errors
+(1,50): doctype-has-public-identifier
 #document
 | <!DOCTYPE html "-//W3C//DTD HTML 4.01//EN" "">
 | <html>
diff --git a/html/testdata/webkit/tests7.dat b/html/testdata/html5lib-tests/tree-construction/tests7.dat
similarity index 88%
rename from html/testdata/webkit/tests7.dat
rename to html/testdata/html5lib-tests/tree-construction/tests7.dat
index 395dc72..aa7e16b 100644
--- a/html/testdata/webkit/tests7.dat
+++ b/html/testdata/html5lib-tests/tree-construction/tests7.dat
@@ -47,6 +47,42 @@
 |   <body>
 
 #data
+<!doctype html></head><base>X
+#errors
+(1,28): unexpected-start-tag-out-of-my-head
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|     <base>
+|   <body>
+|     "X"
+
+#data
+<!doctype html></head><basefont>X
+#errors
+(1,32): unexpected-start-tag-out-of-my-head
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|     <basefont>
+|   <body>
+|     "X"
+
+#data
+<!doctype html></head><bgsound>X
+#errors
+(1,31): unexpected-start-tag-out-of-my-head
+#document
+| <!DOCTYPE html>
+| <html>
+|   <head>
+|     <bgsound>
+|   <body>
+|     "X"
+
+#data
 <!doctype html><table><meta></table>
 #errors
 (1,28): unexpected-start-tag-implies-table-voodoo
@@ -164,7 +200,7 @@
 #data
 <!doctype html><select><input>X
 #errors
-(1,30): unexpected-input-in-select
+1:32: ERROR: Premature end of file. Currently open tags: html, body, select.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -177,7 +213,7 @@
 #data
 <!doctype html><select><select>X
 #errors
-(1,31): unexpected-select-in-select
+1:24: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, select.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -391,7 +427,6 @@
 (1,1): expected-doctype-but-got-chars
 (1,13): foster-parenting-character
 (1,14): foster-parenting-character
-(1,20): foster-parenting-character
 (1,25): unexpected-end-tag
 (1,25): unexpected-end-tag-in-special-element
 (1,26): foster-parenting-character
@@ -408,11 +443,11 @@
 #data
 <select><keygen>
 #errors
-(1,8): expected-doctype-but-got-start-tag
-(1,16): unexpected-input-in-select
+1:1: ERROR: Expected a doctype token
+1:17: ERROR: Premature end of file. Currently open tags: html, body, select.
 #document
 | <html>
 |   <head>
 |   <body>
 |     <select>
-|     <keygen>
+|       <keygen>
diff --git a/html/testdata/webkit/tests8.dat b/html/testdata/html5lib-tests/tree-construction/tests8.dat
similarity index 95%
rename from html/testdata/webkit/tests8.dat
rename to html/testdata/html5lib-tests/tree-construction/tests8.dat
index ba2e63d..d532801 100644
--- a/html/testdata/webkit/tests8.dat
+++ b/html/testdata/html5lib-tests/tree-construction/tests8.dat
@@ -90,6 +90,9 @@
 #data
 <table><li><li></table>
 #errors
+(1,7): expected-doctype-but-got-start-tag
+(1,11): foster-parenting-start-tag
+(1,15): foster-parenting-start-tag
 #document
 | <html>
 |   <head>
diff --git a/html/testdata/webkit/tests9.dat b/html/testdata/html5lib-tests/tree-construction/tests9.dat
similarity index 86%
rename from html/testdata/webkit/tests9.dat
rename to html/testdata/html5lib-tests/tree-construction/tests9.dat
index f8d04b2..1456324 100644
--- a/html/testdata/webkit/tests9.dat
+++ b/html/testdata/html5lib-tests/tree-construction/tests9.dat
@@ -48,20 +48,17 @@
 #data
 <!DOCTYPE html><body><select><math></math></select>
 #errors
-(1,35) unexpected-start-tag-in-select
-(1,42) unexpected-end-tag-in-select
 #document
 | <!DOCTYPE html>
 | <html>
 |   <head>
 |   <body>
 |     <select>
+|       <math math>
 
 #data
 <!DOCTYPE html><body><select><option><math></math></option></select>
 #errors
-(1,43) unexpected-start-tag-in-select
-(1,50) unexpected-end-tag-in-select
 #document
 | <!DOCTYPE html>
 | <html>
@@ -69,6 +66,7 @@
 |   <body>
 |     <select>
 |       <option>
+|         <math math>
 
 #data
 <!DOCTYPE html><body><table><math></math></table>
@@ -301,13 +299,8 @@
 #data
 <!DOCTYPE html><body><table><tr><td><select><math><mi>foo</mi><mi>bar</mi><p>baz</table><p>quux
 #errors
-(1,50) unexpected-start-tag-in-select
-(1,54) unexpected-start-tag-in-select
-(1,62) unexpected-end-tag-in-select
-(1,66) unexpected-start-tag-in-select
-(1,74) unexpected-end-tag-in-select
-(1,77) unexpected-start-tag-in-select
-(1,88) unexpected-table-element-end-tag-in-select-in-table
+1:75: ERROR: Start tag 'p' isn't allowed here. Currently open tags: html, body, table, tbody, tr, td, select, math.
+1:81: ERROR: End tag 'table' isn't allowed here. Currently open tags: html, body, table, tbody, tr, td, select.
 #document
 | <!DOCTYPE html>
 | <html>
@@ -318,28 +311,45 @@
 |         <tr>
 |           <td>
 |             <select>
-|               "foobarbaz"
+|               <math math>
+|                 <math mi>
+|                   "foo"
+|                 <math mi>
+|                   "bar"
+|               <p>
+|                 "baz"
 |     <p>
 |       "quux"
 
 #data
 <!DOCTYPE html><body><table><select><math><mi>foo</mi><mi>bar</mi><p>baz</table><p>quux
 #errors
-(1,36) unexpected-start-tag-implies-table-voodoo
-(1,42) unexpected-start-tag-in-select
-(1,46) unexpected-start-tag-in-select
-(1,54) unexpected-end-tag-in-select
-(1,58) unexpected-start-tag-in-select
-(1,66) unexpected-end-tag-in-select
-(1,69) unexpected-start-tag-in-select
-(1,80) unexpected-table-element-end-tag-in-select-in-table
+1:29: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, table.
+1:37: ERROR: Start tag 'math' isn't allowed here. Currently open tags: html, body, table, select.
+1:47: ERROR: Character tokens aren't legal here
+1:48: ERROR: Character tokens aren't legal here
+1:49: ERROR: Character tokens aren't legal here
+1:59: ERROR: Character tokens aren't legal here
+1:60: ERROR: Character tokens aren't legal here
+1:61: ERROR: Character tokens aren't legal here
+1:67: ERROR: Start tag 'p' isn't allowed here. Currently open tags: html, body, table, select, math.
+1:67: ERROR: Start tag 'p' isn't allowed here. Currently open tags: html, body, table, select.
+1:70: ERROR: Character tokens aren't legal here
+1:71: ERROR: Character tokens aren't legal here
+1:72: ERROR: Character tokens aren't legal here
 #document
 | <!DOCTYPE html>
 | <html>
 |   <head>
 |   <body>
 |     <select>
-|       "foobarbaz"
+|       <math math>
+|         <math mi>
+|           "foo"
+|         <math mi>
+|           "bar"
+|       <p>
+|         "baz"
 |     <table>
 |     <p>
 |       "quux"
diff --git a/html/testdata/webkit/tests_innerHTML_1.dat b/html/testdata/html5lib-tests/tree-construction/tests_innerHTML_1.dat
similarity index 93%
rename from html/testdata/webkit/tests_innerHTML_1.dat
rename to html/testdata/html5lib-tests/tree-construction/tests_innerHTML_1.dat
index 54f4368..09e0456 100644
--- a/html/testdata/webkit/tests_innerHTML_1.dat
+++ b/html/testdata/html5lib-tests/tree-construction/tests_innerHTML_1.dat
@@ -111,16 +111,6 @@
 | <a>
 
 #data
-<a>
-#errors
-(1,3): unexpected-start-tag-implies-table-voodoo
-(1,3): eof-in-table
-#document-fragment
-table
-#document
-| <a>
-
-#data
 <a><caption>a
 #errors
 (1,3): unexpected-start-tag-implies-table-voodoo
@@ -503,30 +493,6 @@
 |   <td>
 
 #data
-<a><td>
-#errors
-(1,3): unexpected-start-tag-implies-table-voodoo
-(1,7): unexpected-cell-in-table-body
-#document-fragment
-tbody
-#document
-| <a>
-| <tr>
-|   <td>
-
-#data
-<a><td>
-#errors
-(1,3): unexpected-start-tag-implies-table-voodoo
-(1,7): unexpected-cell-in-table-body
-#document-fragment
-tbody
-#document
-| <a>
-| <tr>
-|   <td>
-
-#data
 <td><table><tbody><a><tr>
 #errors
 (1,4): unexpected-cell-in-table-body
@@ -649,16 +615,6 @@
 | <td>
 
 #data
-<td><table></table><td>
-#errors
-#document-fragment
-tr
-#document
-| <td>
-|   <table>
-| <td>
-
-#data
 <caption><a>
 #errors
 (1,9): XXX-undefined-error
@@ -834,7 +790,7 @@
 #data
 <input><option>
 #errors
-(1,7): unexpected-input-in-select
+(1,7): XXX-undefined-error
 #document-fragment
 select
 #document
@@ -843,20 +799,21 @@
 #data
 <keygen><option>
 #errors
-(1,8): unexpected-input-in-select
 #document-fragment
 select
 #document
+| <keygen>
 | <option>
 
 #data
 <textarea><option>
 #errors
-(1,10): unexpected-input-in-select
+1:19: ERROR: Premature end of file. Currently open tags: html, textarea.
 #document-fragment
 select
 #document
-| <option>
+| <textarea>
+|   "<option>"
 
 #data
 </html><!--abc-->
diff --git a/html/testdata/webkit/tricky01.dat b/html/testdata/html5lib-tests/tree-construction/tricky01.dat
similarity index 100%
rename from html/testdata/webkit/tricky01.dat
rename to html/testdata/html5lib-tests/tree-construction/tricky01.dat
diff --git a/html/testdata/webkit/webkit01.dat b/html/testdata/html5lib-tests/tree-construction/webkit01.dat
similarity index 87%
rename from html/testdata/webkit/webkit01.dat
rename to html/testdata/html5lib-tests/tree-construction/webkit01.dat
index 2127cfe..fc7f5c8 100644
--- a/html/testdata/webkit/webkit01.dat
+++ b/html/testdata/html5lib-tests/tree-construction/webkit01.dat
@@ -360,6 +360,32 @@
 | <!--  Again  -->
 
 #data
+<html><body></body>
+   <!-- Hi there --></html>
+#errors
+no-doctype
+#document
+| <html>
+|   <head>
+|   <body>
+|     "
+   "
+|   <!--  Hi there  -->
+
+#data
+<html><body></body></html>
+   <!-- Hi there -->
+#errors
+no-doctype
+#document
+| <html>
+|   <head>
+|   <body>
+|     "
+   "
+| <!--  Hi there  -->
+
+#data
 <html><body><ruby><div><rp>xx</rp></div></ruby></body></html>
 #errors
 (1,6): expected-doctype-but-got-start-tag
@@ -411,11 +437,11 @@
 #data
 <select><option>A<select><option>B<select><option>C<select><option>D<select><option>E<select><option>F<select><option>G<select>
 #errors
-(1,8): expected-doctype-but-got-start-tag
-(1,25): unexpected-select-in-select
-(1,59): unexpected-select-in-select
-(1,93): unexpected-select-in-select
-(1,127): unexpected-select-in-select
+1:1: ERROR: Expected a doctype token
+1:18: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, select, option.
+1:52: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, select, option.
+1:86: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, select, option.
+1:120: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, select, option.
 #document
 | <html>
 |   <head>
@@ -502,12 +528,11 @@
 #data
 <kbd><table></kbd><col><select><tr>
 #errors
-(1,5): expected-doctype-but-got-start-tag
-(1,18): unexpected-end-tag-implies-table-voodoo
-(1,18): unexpected-end-tag
-(1,31): unexpected-start-tag-implies-table-voodoo
-(1,35): unexpected-table-element-start-tag-in-select-in-table
-(1,35): eof-in-table
+1:1: ERROR: Expected a doctype token
+1:13: ERROR: End tag 'kbd' isn't allowed here. Currently open tags: html, body, kbd, table.
+1:13: ERROR: End tag 'kbd' isn't allowed here. Currently open tags: html, body, kbd, table.
+1:24: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, kbd, table.
+1:36: ERROR: Premature end of file. Currently open tags: html, body, kbd, table, tbody, tr.
 #document
 | <html>
 |   <head>
@@ -523,12 +548,11 @@
 #data
 <kbd><table></kbd><col><select><tr></table><div>
 #errors
-(1,5): expected-doctype-but-got-start-tag
-(1,18): unexpected-end-tag-implies-table-voodoo
-(1,18): unexpected-end-tag
-(1,31): unexpected-start-tag-implies-table-voodoo
-(1,35): unexpected-table-element-start-tag-in-select-in-table
-(1,48): expected-closing-tag-but-got-eof
+1:1: ERROR: Expected a doctype token
+1:13: ERROR: End tag 'kbd' isn't allowed here. Currently open tags: html, body, kbd, table.
+1:13: ERROR: End tag 'kbd' isn't allowed here. Currently open tags: html, body, kbd, table.
+1:24: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, kbd, table.
+1:49: ERROR: Premature end of file. Currently open tags: html, body, kbd, div.
 #document
 | <html>
 |   <head>
@@ -687,6 +711,10 @@
 #data
 <table><tr><td><svg><desc><td></desc><circle>
 #errors
+(1,7): expected-doctype-but-got-start-tag
+(1,30): unexpected-start-tag
+(1,37): unexpected-end-tag
+(1,22): expected-closing-tag-but-got-eof
 #document
 | <html>
 |   <head>
diff --git a/html/testdata/html5lib-tests/tree-construction/webkit02.dat b/html/testdata/html5lib-tests/tree-construction/webkit02.dat
new file mode 100644
index 0000000..47a3c57
--- /dev/null
+++ b/html/testdata/html5lib-tests/tree-construction/webkit02.dat
@@ -0,0 +1,775 @@
+#data
+<foo bar=qux/>
+#errors
+(1,14): expected-doctype-but-got-start-tag
+(1,14): expected-closing-tag-but-got-eof
+#document
+| <html>
+|   <head>
+|   <body>
+|     <foo>
+|       bar="qux/"
+
+#data
+<p id="status"><noscript><strong>A</strong></noscript><span>B</span></p>
+#errors
+(1,15): expected-doctype-but-got-start-tag
+#script-on
+#document
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       id="status"
+|       <noscript>
+|         "<strong>A</strong>"
+|       <span>
+|         "B"
+
+#data
+<p id="status"><noscript><strong>A</strong></noscript><span>B</span></p>
+#errors
+(1,15): expected-doctype-but-got-start-tag
+#script-off
+#document
+| <html>
+|   <head>
+|   <body>
+|     <p>
+|       id="status"
+|       <noscript>
+|         <strong>
+|           "A"
+|       <span>
+|         "B"
+
+#data
+<div><sarcasm><div></div></sarcasm></div>
+#errors
+(1,5): expected-doctype-but-got-start-tag
+#document
+| <html>
+|   <head>
+|   <body>
+|     <div>
+|       <sarcasm>
+|         <div>
+
+#data
+<html><body><img src="" border="0" alt="><div>A</div></body></html>
+#errors
+(1,6): expected-doctype-but-got-start-tag
+(1,67): eof-in-attribute-value-double-quote
+#new-errors
+(1:68) eof-in-tag
+#document
+| <html>
+|   <head>
+|   <body>
+
+#data
+<table><td></tbody>A
+#errors
+(1,7): expected-doctype-but-got-start-tag
+(1,11): unexpected-cell-in-table-body
+(1,20): foster-parenting-character
+(1,20): eof-in-table
+#document
+| <html>
+|   <head>
+|   <body>
+|     "A"
+|     <table>
+|       <tbody>
+|         <tr>
+|           <td>
+
+#data
+<table><td></thead>A
+#errors
+(1,7): expected-doctype-but-got-start-tag
+(1,11): unexpected-cell-in-table-body
+(1,19): XXX-undefined-error
+(1,20): expected-closing-tag-but-got-eof
+#document
+| <html>
+|   <head>
+|   <body>
+|     <table>
+|       <tbody>
+|         <tr>
+|           <td>
+|             "A"
+
+#data
+<table><td></tfoot>A
+#errors
+(1,7): expected-doctype-but-got-start-tag
+(1,11): unexpected-cell-in-table-body
+(1,19): XXX-undefined-error
+(1,20): expected-closing-tag-but-got-eof
+#document
+| <html>
+|   <head>
+|   <body>
+|     <table>
+|       <tbody>
+|         <tr>
+|           <td>
+|             "A"
+
+#data
+<table><thead><td></tbody>A
+#errors
+(1,7): expected-doctype-but-got-start-tag
+(1,18): unexpected-cell-in-table-body
+(1,26): XXX-undefined-error
+(1,27): expected-closing-tag-but-got-eof
+#document
+| <html>
+|   <head>
+|   <body>
+|     <table>
+|       <thead>
+|         <tr>
+|           <td>
+|             "A"
+
+#data
+<legend>test</legend>
+#errors
+(1,7): expected-doctype-but-got-start-tag
+#document
+| <html>
+|   <head>
+|   <body>
+|     <legend>
+|       "test"
+
+#data
+<table><input>
+#errors
+(1,7): expected-doctype-but-got-start-tag
+(1,14): foster-parenting-start-tag
+(1,15): expected-closing-tag-but-got-eof
+#document
+| <html>
+|   <head>
+|   <body>
+|     <input>
+|     <table>
+
+#data
+<b><em><dcell><postfield><postfield><postfield><postfield><missing_glyph><missing_glyph><missing_glyph><missing_glyph><hkern><aside></b></em>
+#errors
+unexpected-b-end-tag
+unexpected-em-end-tag
+eof-in-aside
+#document-fragment
+div
+#document
+| <b>
+|   <em>
+|     <dcell>
+|       <postfield>
+|         <postfield>
+|           <postfield>
+|             <postfield>
+|               <missing_glyph>
+|                 <missing_glyph>
+|                   <missing_glyph>
+|                     <missing_glyph>
+|                       <hkern>
+| <aside>
+|   <b>
+
+#data
+<b><em><foo><foo><aside></b>
+#errors
+(1,3): expected-doctype-but-got-start-tag
+(1,28): adoption-agency-9
+(1,29): expected-closing-tag-but-got-eof
+#document
+| <html>
+|   <head>
+|   <body>
+|     <b>
+|       <em>
+|         <foo>
+|           <foo>
+|     <em>
+|       <aside>
+|         <b>
+
+#data
+<b><em><foo><foo><aside></b></em>
+#errors
+(1,3): expected-doctype-but-got-start-tag
+(1,28): adoption-agency-9
+(1,33): adoption-agency-9
+(1,34): expected-closing-tag-but-got-eof
+#document
+| <html>
+|   <head>
+|   <body>
+|     <b>
+|       <em>
+|         <foo>
+|           <foo>
+|     <em>
+|     <aside>
+|       <em>
+|         <b>
+
+#data
+<b><em><foo><foo><foo><aside></b>
+#errors
+(1,3): expected-doctype-but-got-start-tag
+(1,33): adoption-agency-9
+(1,34): expected-closing-tag-but-got-eof
+#document
+| <html>
+|   <head>
+|   <body>
+|     <b>
+|       <em>
+|         <foo>
+|           <foo>
+|             <foo>
+|     <aside>
+|       <b>
+
+#data
+<b><em><foo><foo><foo><aside></b></em>
+#errors
+(1,3): expected-doctype-but-got-start-tag
+(1,33): adoption-agency-9
+(1,38): adoption-agency-9
+(1,39): expected-closing-tag-but-got-eof
+#document
+| <html>
+|   <head>
+|   <body>
+|     <b>
+|       <em>
+|         <foo>
+|           <foo>
+|             <foo>
+|     <aside>
+|       <b>
+
+#data
+<b><em><foo><foo><foo><foo><foo><foo><foo><foo><foo><foo><aside></b></em>
+#errors
+(1,68): adoption-agency-9
+(1,73): adoption-agency-9
+(1,74): expected-closing-tag-but-got-eof
+#document-fragment
+div
+#document
+| <b>
+|   <em>
+|     <foo>
+|       <foo>
+|         <foo>
+|           <foo>
+|             <foo>
+|               <foo>
+|                 <foo>
+|                   <foo>
+|                     <foo>
+|                       <foo>
+| <aside>
+|   <b>
+
+#data
+<b><em><foo><foob><foob><foob><foob><fooc><fooc><fooc><fooc><food><aside></b></em>
+#errors
+(1,77): adoption-agency-9
+(1,82): adoption-agency-9
+(1,83): expected-closing-tag-but-got-eof
+#document-fragment
+div
+#document
+| <b>
+|   <em>
+|     <foo>
+|       <foob>
+|         <foob>
+|           <foob>
+|             <foob>
+|               <fooc>
+|                 <fooc>
+|                   <fooc>
+|                     <fooc>
+|                       <food>
+| <aside>
+|   <b>
+
+#data
+<option><XH<optgroup></optgroup>
+#errors
+1:22: ERROR: End tag 'optgroup' isn't allowed here. Currently open tags: html, option, xh<optgroup.
+1:33: ERROR: Premature end of file. Currently open tags: html, option, xh<optgroup.
+#document-fragment
+select
+#document
+| <option>
+|   <xh<optgroup>
+
+#data
+<svg><foreignObject><div>foo</div><plaintext></foreignObject></svg><div>bar</div>
+#errors
+(1,5): expected-doctype-but-got-start-tag
+(1,82): expected-closing-tag-but-got-eof
+#document
+| <html>
+|   <head>
+|   <body>
+|     <svg svg>
+|       <svg foreignObject>
+|         <div>
+|           "foo"
+|         <plaintext>
+|           "</foreignObject></svg><div>bar</div>"
+
+#data
+<svg><foreignObject></foreignObject><title></svg>foo
+#errors
+(1,5): expected-doctype-but-got-start-tag
+(1,49): expected-one-end-tag-but-got-another
+#document
+| <html>
+|   <head>
+|   <body>
+|     <svg svg>
+|       <svg foreignObject>
+|       <svg title>
+|     "foo"
+
+#data
+</foreignObject><plaintext><div>foo</div>
+#errors
+(1,16): expected-doctype-but-got-end-tag
+(1,16): unexpected-end-tag-before-html
+(1,42): expected-closing-tag-but-got-eof
+#document
+| <html>
+|   <head>
+|   <body>
+|     <plaintext>
+|       "<div>foo</div>"
+
+#data
+<svg xml:base xml:lang xml:space xml:baaah definitionurl>
+#errors
+no-doctype
+eof-in-svg
+#document
+| <html>
+|   <head>
+|   <body>
+|     <svg svg>
+|       definitionurl=""
+|       xml lang=""
+|       xml space=""
+|       xml:baaah=""
+|       xml:base=""
+
+#data
+<math definitionurl xlink:title xlink:show>
+#errors
+no-doctype
+eof-in-math
+#document
+| <html>
+|   <head>
+|   <body>
+|     <math math>
+|       definitionURL=""
+|       xlink show=""
+|       xlink title=""
+
+#data
+<math DEFINITIONURL>
+#errors
+no-doctype
+eof-in-math
+#document
+| <html>
+|   <head>
+|   <body>
+|     <math math>
+|       definitionURL=""
+
+#data
+<select><hr>
+#errors
+1:1: ERROR: Expected a doctype token
+1:13: ERROR: Premature end of file. Currently open tags: html, body, select.
+#document
+| <html>
+|   <head>
+|   <body>
+|     <select>
+|       <hr>
+
+#data
+<select><option><hr>
+#errors
+1:1: ERROR: Expected a doctype token
+1:21: ERROR: Premature end of file. Currently open tags: html, body, select.
+#document
+| <html>
+|   <head>
+|   <body>
+|     <select>
+|       <option>
+|       <hr>
+
+#data
+<select><optgroup><option><hr>
+#errors
+1:1: ERROR: Expected a doctype token
+1:31: ERROR: Premature end of file. Currently open tags: html, body, select.
+#document
+| <html>
+|   <head>
+|   <body>
+|     <select>
+|       <optgroup>
+|         <option>
+|       <hr>
+
+#data
+<select><optgroup><hr>
+#errors
+1:1: ERROR: Expected a doctype token
+1:23: ERROR: Premature end of file. Currently open tags: html, body, select, optgroup.
+#document
+| <html>
+|   <head>
+|   <body>
+|     <select>
+|       <optgroup>
+|       <hr>
+
+#data
+<select><option><optgroup><hr>
+#errors
+1:1: ERROR: Expected a doctype token
+1:31: ERROR: Premature end of file. Currently open tags: html, body, select, optgroup.
+#document
+| <html>
+|   <head>
+|   <body>
+|     <select>
+|       <option>
+|       <optgroup>
+|       <hr>
+
+#data
+<table><tr><td><select><hr>
+#errors
+1:1: ERROR: Expected a doctype token
+1:28: ERROR: Premature end of file. Currently open tags: html, body, table, tbody, tr, td, select.
+#document
+| <html>
+|   <head>
+|   <body>
+|     <table>
+|       <tbody>
+|         <tr>
+|           <td>
+|             <select>
+|               <hr>
+
+#data
+<table><tr><td><select><option><hr>
+#errors
+1:1: ERROR: Expected a doctype token
+1:36: ERROR: Premature end of file. Currently open tags: html, body, table, tbody, tr, td, select.
+#document
+| <html>
+|   <head>
+|   <body>
+|     <table>
+|       <tbody>
+|         <tr>
+|           <td>
+|             <select>
+|               <option>
+|               <hr>
+
+#data
+<table><tr><td><select><optgroup><option><hr>
+#errors
+1:1: ERROR: Expected a doctype token
+1:46: ERROR: Premature end of file. Currently open tags: html, body, table, tbody, tr, td, select, optgroup.
+#document
+| <html>
+|   <head>
+|   <body>
+|     <table>
+|       <tbody>
+|         <tr>
+|           <td>
+|             <select>
+|               <optgroup>
+|                 <option>
+|               <hr>
+
+#data
+<table><tr><td><select><optgroup><hr>
+#errors
+1:1: ERROR: Expected a doctype token
+1:38: ERROR: Premature end of file. Currently open tags: html, body, table, tbody, tr, td, select, optgroup.
+#document
+| <html>
+|   <head>
+|   <body>
+|     <table>
+|       <tbody>
+|         <tr>
+|           <td>
+|             <select>
+|               <optgroup>
+|               <hr>
+
+#data
+<table><tr><td><select><option><optgroup><hr>
+#errors
+1:1: ERROR: Expected a doctype token
+1:38: ERROR: Premature end of file. Currently open tags: html, body, table, tbody, tr, td, select, optgroup.
+#document
+| <html>
+|   <head>
+|   <body>
+|     <table>
+|       <tbody>
+|         <tr>
+|           <td>
+|             <select>
+|               <option>
+|               <optgroup>
+|               <hr>
+
+#data
+<select><div><i></div><option>option
+#errors
+1:1: ERROR: Expected a doctype token
+1:17: ERROR: End tag 'div' isn't allowed here. Currently open tags: html, body, select, div, i.
+1:37: ERROR: Premature end of file. Currently open tags: html, body, select, option, i.
+#document
+| <html>
+|   <head>
+|   <body>
+|     <select>
+|       <div>
+|         <i>
+|       <i>
+|         <option>
+|           "option"
+
+#data
+<div><i></div><option>option
+#errors
+1:1: ERROR: Expected a doctype token
+1:9: ERROR: End tag 'div' isn't allowed here. Currently open tags: html, body, div, i.
+1:29: ERROR: Premature end of file. Currently open tags: html, body, i, option.
+#document
+| <html>
+|   <head>
+|   <body>
+|     <div>
+|       <i>
+|     <i>
+|       <option>
+|         "option"
+
+#data
+<select><div>div 1</div><button>button</button><div>div 2</div><datalist><option>option</option></datalist><div>div 3</div></select>
+#errors
+1:1: ERROR: Expected a doctype token
+#document
+| <html>
+|   <head>
+|   <body>
+|     <select>
+|       <div>
+|         "div 1"
+|       <button>
+|         "button"
+|       <div>
+|         "div 2"
+|       <datalist>
+|         <option>
+|           "option"
+|       <div>
+|         "div 3"
+
+#data
+<select><button>button</select>
+#errors
+1:1: ERROR: Expected a doctype token
+1:23: ERROR: End tag 'select' isn't allowed here. Currently open tags: html, body, select, button.
+#document
+| <html>
+|   <head>
+|   <body>
+|     <select>
+|       <button>
+|         "button"
+
+#data
+<select><datalist>datalist</select>
+#errors
+1:1: ERROR: Expected a doctype token
+1:27: ERROR: End tag 'select' isn't allowed here. Currently open tags: html, body, select, datalist.
+#document
+| <html>
+|   <head>
+|   <body>
+|     <select>
+|       <datalist>
+|         "datalist"
+
+#data
+<select><button><select></select></button></select>
+#errors
+1:1: ERROR: Expected a doctype token
+1:17: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, select, button.
+1:25: ERROR: End tag 'select' isn't allowed here. Currently open tags: html, body.
+1:34: ERROR: End tag 'button' isn't allowed here. Currently open tags: html, body.
+1:43: ERROR: End tag 'select' isn't allowed here. Currently open tags: html, body.
+#document
+| <html>
+|   <head>
+|   <body>
+|     <select>
+|       <button>
+
+#data
+<select><button><div><select></select>
+#errors
+1:1: ERROR: Expected a doctype token
+1:22: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, select, button, div.
+1:30: ERROR: End tag 'select' isn't allowed here. Currently open tags: html, body.
+#document
+| <html>
+|   <head>
+|   <body>
+|     <select>
+|       <button>
+|         <div>
+
+#data
+<select><div><option><img>option</option></div></select>
+#errors
+1:1: ERROR: Expected a doctype token
+#document
+| <html>
+|   <head>
+|   <body>
+|     <select>
+|       <div>
+|         <option>
+|           <img>
+|           "option"
+
+#data
+<select><input>
+#errors
+1:1: ERROR: Expected a doctype token
+1:16: ERROR: Premature end of file. Currently open tags: html, body, select.
+#document
+| <html>
+|   <head>
+|   <body>
+|     <select>
+|     <input>
+
+#data
+<select><button><selectedcontent></button><option>X
+#errors
+#document
+| <html>
+|   <head>
+|   <body>
+|     <select>
+|       <button>
+|         <selectedcontent>
+|           "X"
+|       <option>
+|         "X"
+
+#data
+<select><button><selectedcontent></button><option>x<i>i<b>ib</i>b
+#errors
+#document
+| <html>
+|   <head>
+|   <body>
+|     <select>
+|       <button>
+|         <selectedcontent>
+|           "x"
+|           <i>
+|             "i"
+|             <b>
+|               "ib"
+|           <b>
+|             "b"
+|       <option>
+|         "x"
+|         <i>
+|           "i"
+|           <b>
+|             "ib"
+|         <b>
+|           "b"
+
+#data
+<select><button><selectedcontent></button><option>X<option>Y
+#errors
+#document
+| <html>
+|   <head>
+|   <body>
+|     <select>
+|       <button>
+|         <selectedcontent>
+|           "X"
+|       <option>
+|         "X"
+|       <option>
+|         "Y"
+
+#data
+<select><button><selectedcontent></button><option>X<option selected>Y
+#errors
+#document
+| <html>
+|   <head>
+|   <body>
+|     <select>
+|       <button>
+|         <selectedcontent>
+|           "Y"
+|       <option>
+|         "X"
+|       <option>
+|         selected=""
+|         "Y"
+
+#data
+<font><select><option>a</option></font></select>
+#errors
+#document
+| <html>
+|   <head>
+|   <body>
+|     <font>
+|       <select>
+|         <option>
+|           "a"
diff --git a/html/testdata/webkit/README b/html/testdata/webkit/README
deleted file mode 100644
index 9b4c2d8..0000000
--- a/html/testdata/webkit/README
+++ /dev/null
@@ -1,28 +0,0 @@
-The *.dat files in this directory are copied from The WebKit Open Source
-Project, specifically $WEBKITROOT/LayoutTests/html5lib/resources.
-WebKit is licensed under a BSD style license.
-http://webkit.org/coding/bsd-license.html says:
-
-Copyright (C) 2009 Apple Inc. All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-1. Redistributions of source code must retain the above copyright notice,
-this list of conditions and the following disclaimer.
-
-2. Redistributions in binary form must reproduce the above copyright notice,
-this list of conditions and the following disclaimer in the documentation
-and/or other materials provided with the distribution.
-
-THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS" AND ANY
-EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
-DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
-ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
diff --git a/html/testdata/webkit/math.dat b/html/testdata/webkit/math.dat
deleted file mode 100644
index ae9cd7c..0000000
--- a/html/testdata/webkit/math.dat
+++ /dev/null
@@ -1,81 +0,0 @@
-#data
-<math><tr><td><mo><tr>
-#errors
-#document-fragment
-td
-#document
-| <math math>
-|   <math tr>
-|     <math td>
-|       <math mo>
-
-#data
-<math><tr><td><mo><tr>
-#errors
-#document-fragment
-tr
-#document
-| <math math>
-|   <math tr>
-|     <math td>
-|       <math mo>
-
-#data
-<math><thead><mo><tbody>
-#errors
-#document-fragment
-thead
-#document
-| <math math>
-|   <math thead>
-|     <math mo>
-
-#data
-<math><tfoot><mo><tbody>
-#errors
-#document-fragment
-tfoot
-#document
-| <math math>
-|   <math tfoot>
-|     <math mo>
-
-#data
-<math><tbody><mo><tfoot>
-#errors
-#document-fragment
-tbody
-#document
-| <math math>
-|   <math tbody>
-|     <math mo>
-
-#data
-<math><tbody><mo></table>
-#errors
-#document-fragment
-tbody
-#document
-| <math math>
-|   <math tbody>
-|     <math mo>
-
-#data
-<math><thead><mo></table>
-#errors
-#document-fragment
-tbody
-#document
-| <math math>
-|   <math thead>
-|     <math mo>
-
-#data
-<math><tfoot><mo></table>
-#errors
-#document-fragment
-tbody
-#document
-| <math math>
-|   <math tfoot>
-|     <math mo>
diff --git a/html/testdata/webkit/namespace-sensitivity.dat b/html/testdata/webkit/namespace-sensitivity.dat
deleted file mode 100644
index ca35c0e..0000000
--- a/html/testdata/webkit/namespace-sensitivity.dat
+++ /dev/null
@@ -1,16 +0,0 @@
-#data
-<body><table><tr><td><svg><td><foreignObject><span></td>Foo
-#errors
-#document
-| <html>
-|   <head>
-|   <body>
-|     "Foo"
-|     <table>
-|       <tbody>
-|         <tr>
-|           <td>
-|             <svg svg>
-|               <svg td>
-|                 <svg foreignObject>
-|                   <span>
diff --git a/html/testdata/webkit/webkit02.dat b/html/testdata/webkit/webkit02.dat
deleted file mode 100644
index 791991d..0000000
--- a/html/testdata/webkit/webkit02.dat
+++ /dev/null
@@ -1,303 +0,0 @@
-#data
-<foo bar=qux/>
-#errors
-(1,14): expected-doctype-but-got-start-tag
-(1,14): expected-closing-tag-but-got-eof
-#document
-| <html>
-|   <head>
-|   <body>
-|     <foo>
-|       bar="qux/"
-
-#data
-<p id="status"><noscript><strong>A</strong></noscript><span>B</span></p>
-#errors
-(1,15): expected-doctype-but-got-start-tag
-#script-on
-#document
-| <html>
-|   <head>
-|   <body>
-|     <p>
-|       id="status"
-|       <noscript>
-|         "<strong>A</strong>"
-|       <span>
-|         "B"
-
-#data
-<p id="status"><noscript><strong>A</strong></noscript><span>B</span></p>
-#errors
-(1,15): expected-doctype-but-got-start-tag
-#script-off
-#document
-| <html>
-|   <head>
-|   <body>
-|     <p>
-|       id="status"
-|       <noscript>
-|         <strong>
-|           "A"
-|       <span>
-|         "B"
-
-#data
-<div><sarcasm><div></div></sarcasm></div>
-#errors
-(1,5): expected-doctype-but-got-start-tag
-#document
-| <html>
-|   <head>
-|   <body>
-|     <div>
-|       <sarcasm>
-|         <div>
-
-#data
-<html><body><img src="" border="0" alt="><div>A</div></body></html>
-#errors
-(1,6): expected-doctype-but-got-start-tag
-(1,67): eof-in-attribute-value-double-quote
-#new-errors
-(1:68) eof-in-tag
-#document
-| <html>
-|   <head>
-|   <body>
-
-#data
-<table><td></tbody>A
-#errors
-(1,7): expected-doctype-but-got-start-tag
-(1,11): unexpected-cell-in-table-body
-(1,20): foster-parenting-character
-(1,20): eof-in-table
-#document
-| <html>
-|   <head>
-|   <body>
-|     "A"
-|     <table>
-|       <tbody>
-|         <tr>
-|           <td>
-
-#data
-<table><td></thead>A
-#errors
-(1,7): expected-doctype-but-got-start-tag
-(1,11): unexpected-cell-in-table-body
-(1,19): XXX-undefined-error
-(1,20): expected-closing-tag-but-got-eof
-#document
-| <html>
-|   <head>
-|   <body>
-|     <table>
-|       <tbody>
-|         <tr>
-|           <td>
-|             "A"
-
-#data
-<table><td></tfoot>A
-#errors
-(1,7): expected-doctype-but-got-start-tag
-(1,11): unexpected-cell-in-table-body
-(1,19): XXX-undefined-error
-(1,20): expected-closing-tag-but-got-eof
-#document
-| <html>
-|   <head>
-|   <body>
-|     <table>
-|       <tbody>
-|         <tr>
-|           <td>
-|             "A"
-
-#data
-<table><thead><td></tbody>A
-#errors
-(1,7): expected-doctype-but-got-start-tag
-(1,18): unexpected-cell-in-table-body
-(1,26): XXX-undefined-error
-(1,27): expected-closing-tag-but-got-eof
-#document
-| <html>
-|   <head>
-|   <body>
-|     <table>
-|       <thead>
-|         <tr>
-|           <td>
-|             "A"
-
-#data
-<legend>test</legend>
-#errors
-#document
-| <html>
-|   <head>
-|   <body>
-|     <legend>
-|       "test"
-
-#data
-<table><input>
-#errors
-#document
-| <html>
-|   <head>
-|   <body>
-|     <input>
-|     <table>
-
-#data
-<b><em><foo><foo><aside></b>
-#errors
-#document
-| <html>
-|   <head>
-|   <body>
-|     <b>
-|       <em>
-|         <foo>
-|           <foo>
-|     <em>
-|       <aside>
-|         <b>
-
-#data
-<b><em><foo><foo><aside></b></em>
-#errors
-#document
-| <html>
-|   <head>
-|   <body>
-|     <b>
-|       <em>
-|         <foo>
-|           <foo>
-|     <em>
-|     <aside>
-|       <em>
-|         <b>
-
-#data
-<b><em><foo><foo><foo><aside></b>
-#errors
-#document
-| <html>
-|   <head>
-|   <body>
-|     <b>
-|       <em>
-|         <foo>
-|           <foo>
-|             <foo>
-|     <aside>
-|       <b>
-
-#data
-<b><em><foo><foo><foo><aside></b></em>
-#errors
-#document
-| <html>
-|   <head>
-|   <body>
-|     <b>
-|       <em>
-|         <foo>
-|           <foo>
-|             <foo>
-|     <aside>
-|       <b>
-
-#data
-<b><em><foo><foo><foo><foo><foo><foo><foo><foo><foo><foo><aside></b></em>
-#errors
-#document-fragment
-div
-#document
-| <b>
-|   <em>
-|     <foo>
-|       <foo>
-|         <foo>
-|           <foo>
-|             <foo>
-|               <foo>
-|                 <foo>
-|                   <foo>
-|                     <foo>
-|                       <foo>
-| <aside>
-|   <b>
-
-#data
-<b><em><foo><foob><foob><foob><foob><fooc><fooc><fooc><fooc><food><aside></b></em>
-#errors
-#document-fragment
-div
-#document
-| <b>
-|   <em>
-|     <foo>
-|       <foob>
-|         <foob>
-|           <foob>
-|             <foob>
-|               <fooc>
-|                 <fooc>
-|                   <fooc>
-|                     <fooc>
-|                       <food>
-| <aside>
-|   <b>
-
-#data
-<option><XH<optgroup></optgroup>
-#errors
-#document-fragment
-select
-#document
-| <option>
-
-#data
-<svg><foreignObject><div>foo</div><plaintext></foreignObject></svg><div>bar</div>
-#errors
-#document
-| <html>
-|   <head>
-|   <body>
-|     <svg svg>
-|       <svg foreignObject>
-|         <div>
-|           "foo"
-|         <plaintext>
-|           "</foreignObject></svg><div>bar</div>"
-
-#data
-<svg><foreignObject></foreignObject><title></svg>foo
-#errors
-#document
-| <html>
-|   <head>
-|   <body>
-|     <svg svg>
-|       <svg foreignObject>
-|       <svg title>
-|     "foo"
-
-#data
-</foreignObject><plaintext><div>foo</div>
-#errors
-#document
-| <html>
-|   <head>
-|   <body>
-|     <plaintext>
-|       "<div>foo</div>"
diff --git a/html/token.go b/html/token.go
index 09b0e7e..83fcf96 100644
--- a/html/token.go
+++ b/html/token.go
@@ -1300,9 +1300,22 @@
 		attrNames: make(map[string]bool),
 	}
 	if contextTag != "" {
+		// Per the "Parsing HTML Fragments" portion of the spec:
+		//    For performance reasons, an implementation that does not report errors
+		//    and that uses the actual state machine described in this specification
+		//    directly could use the PLAINTEXT state instead of the RAWTEXT and script
+		//    data states where those are mentioned in the list above. Except for
+		//    rules regarding parse errors, they are equivalent, since there is no
+		//    appropriate end tag token in the fragment case, yet they involve far
+		//    fewer state transitions.
+		//
+		// As such we just set everything to plaintext, which makes some complex parsing
+		// cases somewhat simpler.
 		switch s := strings.ToLower(contextTag); s {
-		case "iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style", "title", "textarea", "xmp":
+		case "title", "textarea":
 			z.rawTag = s
+		case "style", "xmp", "iframe", "noembed", "noframes", "script", "noscript", "plaintext":
+			z.rawTag = "plaintext"
 		}
 	}
 	return z
diff --git a/http2/export_wrap_test.go b/http2/export_wrap_test.go
index 7cb8a56..63fa3fc 100644
--- a/http2/export_wrap_test.go
+++ b/http2/export_wrap_test.go
@@ -9,8 +9,7 @@
 import "net/http"
 
 func (t *Transport) TestTransport() *http.Transport {
-	t.init()
-	return t.t1
+	return t.init()
 }
 
 func (s *Server) TestSetNewConnFunc(f func(*ServerConn)) {
diff --git a/http2/transport_configure_test.go b/http2/transport_configure_test.go
new file mode 100644
index 0000000..83993c6
--- /dev/null
+++ b/http2/transport_configure_test.go
@@ -0,0 +1,63 @@
+// Copyright 2026 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 http2_test
+
+import (
+	"crypto/tls"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	. "golang.org/x/net/http2"
+)
+
+// TestConfigureTransportEnablesHTTP2 verifies that ConfigureTransport and
+// ConfigureTransports enable HTTP/2 on a transport with a custom TLSClientConfig.
+// net/http does not auto-enable HTTP/2 for such a transport, so without
+// ConfigureTransport doing so the request is sent over HTTP/1 to an HTTP/2
+// server. The test is intentionally not build-tagged: the behavior must hold for
+// the legacy implementation (transport.go) and the go1.27 net/http wrapper
+// (transport_wrap.go) alike.
+func TestConfigureTransportEnablesHTTP2(t *testing.T) {
+	for _, tc := range []struct {
+		name      string
+		configure func(*http.Transport) error
+	}{
+		{"ConfigureTransport", ConfigureTransport},
+		{"ConfigureTransports", func(t1 *http.Transport) error {
+			_, err := ConfigureTransports(t1)
+			return err
+		}},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				io.WriteString(w, r.Proto)
+			}))
+			if err := ConfigureServer(ts.Config, &Server{}); err != nil {
+				t.Fatal(err)
+			}
+			ts.TLS = ts.Config.TLSConfig
+			ts.StartTLS()
+			defer ts.Close()
+
+			// A custom TLSClientConfig disables net/http's automatic HTTP/2;
+			// ConfigureTransport is responsible for re-enabling it.
+			tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
+			if err := tc.configure(tr); err != nil {
+				t.Fatal(err)
+			}
+			res, err := (&http.Client{Transport: tr}).Get(ts.URL)
+			if err != nil {
+				t.Fatalf("Get: %v", err)
+			}
+			defer res.Body.Close()
+			body, _ := io.ReadAll(res.Body)
+			if res.ProtoMajor != 2 {
+				t.Errorf("request negotiated %s (server saw %q); want HTTP/2", res.Proto, body)
+			}
+		})
+	}
+}
diff --git a/http2/transport_wrap.go b/http2/transport_wrap.go
index d25d99b..534e77a 100644
--- a/http2/transport_wrap.go
+++ b/http2/transport_wrap.go
@@ -22,8 +22,8 @@
 )
 
 func configureTransport(t1 *http.Transport) error {
-	// ConfigureTransport is a no-op: The http.Transport already supports HTTP/2.
-	return nil
+	_, err := configureTransports(t1)
+	return err
 }
 
 func configureTransports(t1 *http.Transport) (*Transport, error) {
@@ -31,6 +31,17 @@
 	// linked to the http.Transport's.
 	tr2 := &Transport{}
 	tr2.configure(t1)
+	// Enable HTTP/2 on the transport, as the pre-wrapping implementation did:
+	// net/http does not auto-enable it for a transport with a custom
+	// TLSClientConfig or dialer.
+	if t1.TLSClientConfig == nil {
+		t1.TLSClientConfig = &tls.Config{}
+	}
+	if t1.Protocols == nil {
+		t1.Protocols = new(http.Protocols)
+		t1.Protocols.SetHTTP1(true)
+	}
+	t1.Protocols.SetHTTP2(true)
 	return tr2, nil
 }
 
@@ -44,7 +55,7 @@
 // Registered is called by net/http.Transport.RegisterProtocol,
 // to let us know that it understands the registration mechanism we're using.
 func (t transportConfig) Registered(t1 *http.Transport) {
-	t.t.t1 = t1
+	t.t.lazyt1 = t1
 }
 
 func (t transportConfig) DisableCompression() bool {
@@ -134,29 +145,30 @@
 
 type transportInternal struct {
 	initOnce sync.Once
-	t1       *http.Transport
+	lazyt1   *http.Transport
 }
 
-func (t *Transport) init() {
+func (t *Transport) init() *http.Transport {
 	t.initOnce.Do(func() {
-		if t.t1 != nil {
+		if t.lazyt1 != nil {
 			return
 		}
 		t1 := &http.Transport{}
 		t.configure(t1)
 	})
+	return t.lazyt1
 }
 
 func (t *Transport) configure(t1 *http.Transport) {
 	t1.RegisterProtocol("http/2", transportConfig{t})
-	// tr2.t1 is set by transportConfig.Registered.
-	if t.t1 != t1 {
+	// tr2.lazyt1 is set by transportConfig.Registered.
+	if t.lazyt1 != t1 {
 		panic("http2: net/http does not support this version of x/net/http2")
 	}
 }
 
 func (t *Transport) roundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) {
-	t.init()
+	t1 := t.init()
 
 	if req.URL.Scheme == "http" && !t.AllowHTTP {
 		return nil, errors.New("http2: unencrypted HTTP/2 not enabled")
@@ -177,22 +189,23 @@
 	ctx := context.WithValue(req.Context(), http2TransportContextKey{}, t)
 	req = req.WithContext(ctx)
 
-	return t.t1.RoundTrip(req)
+	return t1.RoundTrip(req)
 }
 
 func (t *Transport) closeIdleConnections() {
-	t.init()
-	t.t1.CloseIdleConnections()
+	t1 := t.init()
+	t1.CloseIdleConnections()
 }
 
 func (t *Transport) newUserClientConn(c net.Conn) (*ClientConn, error) {
+	t1 := t.init()
 	// http.Transport's NewClientConn doesn't provide a supported way to create
 	// a connection from a net.Conn. (This might be useful to add in the future?)
 	// We're going to craftily sneak one in via the context key, with the
 	// scheme of "http/2" telling NewClientConn to look for it.
 	ctx := context.WithValue(context.Background(), netConnContextKey{}, c)
 
-	nhcc, err := t.t1.NewClientConn(ctx, "http/2", "")
+	nhcc, err := t1.NewClientConn(ctx, "http/2", "")
 	if err != nil {
 		return nil, err
 	}
diff --git a/idna/idna.go b/idna/idna.go
index 2276712..e2f28fe 100644
--- a/idna/idna.go
+++ b/idna/idna.go
@@ -400,7 +400,11 @@
 				// Spec says keep the old label.
 				continue
 			}
-			if unicode16 && err == nil && len(u) > 0 && isASCII(u) {
+			if err == nil && len(u) > 0 && isASCII(u) {
+				// UTS 43 pre-revision 33 doesn't classify a xn-- label
+				// which contains only ASCII characters as an error,
+				// but that's a specification bug and a security issue.
+				// Always return an error in this case.
 				err = punyError(enc)
 			}
 			isBidi = isBidi || bidirule.DirectionString(u) != bidi.LeftToRight
diff --git a/idna/idna_test.go b/idna/idna_test.go
index 0b067ca..2d58bff 100644
--- a/idna/idna_test.go
+++ b/idna/idna_test.go
@@ -104,5 +104,15 @@
 	}
 }
 
-// TODO(nigeltao): test errors, once we've specified when ToASCII and ToUnicode
-// return errors.
+func TestIDNAErrors(t *testing.T) {
+	for _, tc := range []string{
+		"xn--example-.com",
+	} {
+		if got, err := ToASCII(tc); err == nil {
+			t.Errorf("ToASCII(%q) = %q, want error", tc, got)
+		}
+		if got, err := ToUnicode(tc); err == nil {
+			t.Errorf("ToUnicode(%q) = %q, want error", tc, got)
+		}
+	}
+}
diff --git a/internal/http3/body.go b/internal/http3/body.go
index 169417c..e025f5d 100644
--- a/internal/http3/body.go
+++ b/internal/http3/body.go
@@ -108,8 +108,8 @@
 		w.st.writeVarint(int64(len(encTrailer)))
 		w.st.Write(encTrailer)
 	}
-	if w.st != nil && w.st.stream != nil {
-		w.st.stream.CloseWrite()
+	if w.st != nil {
+		w.st.CloseWrite()
 	}
 	return nil
 }
@@ -127,9 +127,12 @@
 	send100Continue func()
 	// A map where the key represents the trailer header names we expect. If
 	// there is a HEADERS frame after reading DATA frames to EOF, the value of
-	// the headers will be written here, provided that the name of the header
-	// exists in the map already.
-	trailer http.Header
+	// the headers will be written here. Keys in the map are assumed to be
+	// canonicalized.
+	// If filterTrailer is true, headers that are not already in the map will
+	// be ignored; otherwise, all headers will be added to the map.
+	trailer       http.Header
+	filterTrailer bool
 }
 
 func (r *bodyReader) Read(p []byte) (n int, err error) {
@@ -188,8 +191,16 @@
 			}
 			var dec qpackDecoder
 			if err := dec.decode(r.st, func(_ indexType, name, value string) error {
+				if r.trailer == nil {
+					return nil
+				}
+				if !validWireHeaderFieldName(name) || !httpguts.ValidHeaderFieldValue(value) {
+					return nil
+				}
 				name = textproto.CanonicalMIMEHeaderKey(textproto.TrimString(name))
-				if _, ok := r.trailer[name]; ok {
+				if !r.filterTrailer {
+					r.trailer.Add(name, value)
+				} else if _, ok := r.trailer[name]; ok {
 					r.trailer.Add(name, value)
 				}
 				return nil
@@ -222,9 +233,11 @@
 func (r *bodyReader) Close() error {
 	// Unlike the HTTP/1 and HTTP/2 body readers (at the time of this comment being written),
 	// calling Close concurrently with Read will interrupt the read.
-	r.st.stream.CloseRead()
+	r.st.CloseRead()
 	// Make sure that any data that has already been written to bodyReader
 	// cannot be read after it has been closed.
+	r.mu.Lock()
+	defer r.mu.Unlock()
 	r.err = net.ErrClosed
 	r.remain = 0
 	return nil
diff --git a/internal/http3/body_test.go b/internal/http3/body_test.go
index ae93014..e10d062 100644
--- a/internal/http3/body_test.go
+++ b/internal/http3/body_test.go
@@ -224,7 +224,7 @@
 				case receiveData:
 					t.Logf("receive DATA frame content: size=%v", step.size)
 					for range step.size {
-						st.stream.stream.WriteByte(byte(bytesSent))
+						st.WriteByte(byte(bytesSent))
 						bytesSent++
 					}
 					st.Flush()
@@ -239,7 +239,7 @@
 					st.Flush()
 				case receiveEOF:
 					t.Logf("receive EOF on request stream")
-					st.stream.stream.CloseWrite()
+					st.CloseWrite()
 				case wantBody:
 					t.Logf("read %v bytes from response body", step.size)
 					want := make([]byte, step.size)
diff --git a/internal/http3/conn.go b/internal/http3/conn.go
index 6a3c962..a90464e 100644
--- a/internal/http3/conn.go
+++ b/internal/http3/conn.go
@@ -99,14 +99,14 @@
 	case *connectionError:
 		h.abort(err)
 	case nil:
-		st.stream.CloseRead()
-		st.stream.CloseWrite()
+		st.CloseRead()
+		st.CloseWrite()
 	case *streamError:
-		st.stream.CloseRead()
-		st.stream.Reset(uint64(err.code))
+		st.CloseRead()
+		st.Reset(uint64(err.code))
 	default:
-		st.stream.CloseRead()
-		st.stream.Reset(uint64(errH3InternalError))
+		st.CloseRead()
+		st.Reset(uint64(errH3InternalError))
 	}
 }
 
diff --git a/internal/http3/conn_test.go b/internal/http3/conn_test.go
index 68c665e..91eb404 100644
--- a/internal/http3/conn_test.go
+++ b/internal/http3/conn_test.go
@@ -132,7 +132,7 @@
 			t.Fatal(err)
 		}
 		st := newTestQUICStream(tc.t, newStream(qs))
-		st.stream.stream.Close()
+		st.Close()
 
 		tc.wantClosed("after peer creates and closes uni stream", errH3StreamCreationError)
 	})
diff --git a/internal/http3/gzip.go b/internal/http3/gzip.go
new file mode 100644
index 0000000..8854f2b
--- /dev/null
+++ b/internal/http3/gzip.go
@@ -0,0 +1,125 @@
+// Copyright 2026 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 http3
+
+import (
+	"compress/flate"
+	"compress/gzip"
+	"errors"
+	"io"
+	"io/fs"
+	"sync"
+)
+
+var errConcurrentReadOnResBody = errors.New("http3: concurrent read on response body")
+
+// gzipReader wraps a response body so it can lazily
+// get gzip.Reader from the pool on the first call to Read.
+// After Close is called it puts gzip.Reader to the pool immediately
+// if there is no Read in progress or later when Read completes.
+type gzipReader struct {
+	body io.ReadCloser // underlying Response.Body
+	mu   sync.Mutex    // guards zr and zerr
+	zr   *gzip.Reader  // stores gzip reader from the pool between reads
+	zerr error         // sticky gzip reader init error or sentinel value to detect concurrent read and read after close
+}
+
+type eofReader struct{}
+
+func (eofReader) Read([]byte) (int, error) { return 0, io.EOF }
+func (eofReader) ReadByte() (byte, error)  { return 0, io.EOF }
+
+var gzipPool = sync.Pool{New: func() any { return new(gzip.Reader) }}
+
+// gzipPoolGet gets a gzip.Reader from the pool and resets it to read from r.
+func gzipPoolGet(r io.Reader) (*gzip.Reader, error) {
+	zr := gzipPool.Get().(*gzip.Reader)
+	if err := zr.Reset(r); err != nil {
+		gzipPoolPut(zr)
+		return nil, err
+	}
+	return zr, nil
+}
+
+// gzipPoolPut puts a gzip.Reader back into the pool.
+func gzipPoolPut(zr *gzip.Reader) {
+	// Reset will allocate bufio.Reader if we pass it anything
+	// other than a flate.Reader, so ensure that it's getting one.
+	var r flate.Reader = eofReader{}
+	zr.Reset(r)
+	gzipPool.Put(zr)
+}
+
+// acquire returns a gzip.Reader for reading response body.
+// The reader must be released after use.
+func (gz *gzipReader) acquire() (*gzip.Reader, error) {
+	gz.mu.Lock()
+	defer gz.mu.Unlock()
+	if gz.zerr != nil {
+		return nil, gz.zerr
+	}
+	if gz.zr == nil {
+		// gzipPoolGet might block indefinitely since it reads the gzip header.
+		// Therefore, drop mu temporarily when using gzipPoolGet.
+		// We set zerr to errConcurrentReadOnResBody to prevent concurrent read
+		// even when mu is temporarily dropped.
+		gz.zerr = errConcurrentReadOnResBody
+		gz.mu.Unlock()
+		zr, err := gzipPoolGet(gz.body)
+		gz.mu.Lock()
+		// Guard against Close being called while gzipPoolGet is running.
+		if gz.zerr != errConcurrentReadOnResBody {
+			if zr != nil {
+				gzipPoolPut(zr)
+			}
+			return nil, gz.zerr
+		}
+		gz.zr, gz.zerr = zr, err
+		if gz.zerr != nil {
+			return nil, gz.zerr
+		}
+	}
+	ret := gz.zr
+	gz.zr, gz.zerr = nil, errConcurrentReadOnResBody
+	return ret, nil
+}
+
+// release returns the gzip.Reader to the pool if Close was called during Read.
+func (gz *gzipReader) release(zr *gzip.Reader) {
+	gz.mu.Lock()
+	defer gz.mu.Unlock()
+	if gz.zerr == errConcurrentReadOnResBody {
+		gz.zr, gz.zerr = zr, nil
+	} else { // fs.ErrClosed
+		gzipPoolPut(zr)
+	}
+}
+
+// close returns the gzip.Reader to the pool immediately or
+// signals release to do so after Read completes.
+func (gz *gzipReader) close() {
+	gz.mu.Lock()
+	defer gz.mu.Unlock()
+	if gz.zerr == nil && gz.zr != nil {
+		gzipPoolPut(gz.zr)
+		gz.zr = nil
+	}
+	gz.zerr = fs.ErrClosed
+}
+
+func (gz *gzipReader) Read(p []byte) (n int, err error) {
+	zr, err := gz.acquire()
+	if err != nil {
+		return 0, err
+	}
+	defer gz.release(zr)
+
+	return zr.Read(p)
+}
+
+func (gz *gzipReader) Close() error {
+	gz.close()
+
+	return gz.body.Close()
+}
diff --git a/internal/http3/qpack.go b/internal/http3/qpack.go
index e400d83..423b5aa 100644
--- a/internal/http3/qpack.go
+++ b/internal/http3/qpack.go
@@ -5,8 +5,10 @@
 package http3
 
 import (
+	"encoding/binary"
 	"errors"
 	"io"
+	"math"
 
 	"golang.org/x/net/http/httpguts"
 	"golang.org/x/net/http2/hpack"
@@ -225,25 +227,19 @@
 
 // readPrefixedIntWithByte reads an RFC 7541 prefixed integer from st.
 // The first byte has already been read from the stream.
-func (st *stream) readPrefixedIntWithByte(firstByte byte, prefixLen uint8) (v int64, err error) {
+func (st *stream) readPrefixedIntWithByte(firstByte byte, prefixLen uint8) (int64, error) {
 	prefixMask := (byte(1) << prefixLen) - 1
-	v = int64(firstByte & prefixMask)
-	if v != int64(prefixMask) {
-		return v, nil
+	if v := firstByte & prefixMask; v != prefixMask {
+		return int64(v), nil
 	}
-	m := 0
-	for {
-		b, err := st.ReadByte()
-		if err != nil {
-			return 0, errQPACKDecompressionFailed
-		}
-		v += int64(b&127) << m
-		m += 7
-		if b&128 == 0 {
-			break
-		}
+	v, err := binary.ReadUvarint(st)
+	if err != nil {
+		return 0, errQPACKDecompressionFailed
 	}
-	return v, err
+	if v > math.MaxInt64-uint64(prefixMask) {
+		return 0, errQPACKDecompressionFailed
+	}
+	return int64(v + uint64(prefixMask)), nil
 }
 
 // appendPrefixedInt appends an RFC 7541 prefixed integer to b.
@@ -258,11 +254,7 @@
 	}
 	b = append(b, firstByte|byte(prefixMask))
 	u -= prefixMask
-	for u >= 128 {
-		b = append(b, 0x80|byte(u&0x7f))
-		u >>= 7
-	}
-	return append(b, byte(u))
+	return binary.AppendUvarint(b, u)
 }
 
 // String literal encoding from RFC 7541, section 5.2
@@ -291,6 +283,9 @@
 	if err != nil {
 		return "", errQPACKDecompressionFailed
 	}
+	if st.lim >= 0 && size > st.lim {
+		return "", errQPACKDecompressionFailed
+	}
 
 	hbit := byte(1) << prefixLen
 	isHuffman := firstByte&hbit != 0
diff --git a/internal/http3/qpack_decode_test.go b/internal/http3/qpack_decode_test.go
index 59e67b0..34afc8d 100644
--- a/internal/http3/qpack_decode_test.go
+++ b/internal/http3/qpack_decode_test.go
@@ -173,6 +173,12 @@
 	}, {
 		name: "too high static table index",
 		enc:  unhex("0000ff23ff24"),
+	}, {
+		name: "prefixed string length overflow",
+		enc:  unhex("000027ffffffffffffffff7f"),
+	}, {
+		name: "prefixed static index overflow",
+		enc:  unhex("0000ffffffffffffffffff7f"),
 	}} {
 		synctestSubtest(t, test.name, func(t *testing.T) {
 			st1, st2 := newStreamPair(t)
diff --git a/internal/http3/qpack_static.go b/internal/http3/qpack_static.go
index 6c0b51c..395554c 100644
--- a/internal/http3/qpack_static.go
+++ b/internal/http3/qpack_static.go
@@ -13,7 +13,7 @@
 
 // staticTableEntry returns the static table entry with the given index.
 func staticTableEntry(index int64) (tableEntry, error) {
-	if index >= int64(len(staticTableEntries)) {
+	if index < 0 || index >= int64(len(staticTableEntries)) {
 		return tableEntry{}, errQPACKDecompressionFailed
 	}
 	return staticTableEntries[index], nil
diff --git a/internal/http3/roundtrip.go b/internal/http3/roundtrip.go
index edf5f13..c5aab72 100644
--- a/internal/http3/roundtrip.go
+++ b/internal/http3/roundtrip.go
@@ -11,6 +11,7 @@
 	"net/http/httptrace"
 	"net/textproto"
 	"strconv"
+	"strings"
 	"sync"
 
 	"golang.org/x/net/http/httpguts"
@@ -41,15 +42,21 @@
 func (rt *roundTripState) abort(err error) error {
 	rt.errOnce.Do(func() {
 		rt.err = err
+
+		rt.cc.mu.Lock()
+		rt.cc.active--
+		rt.cc.mu.Unlock()
+		rt.cc.maybeCallStateHook()
+
 		switch e := err.(type) {
 		case *connectionError:
 			rt.cc.abort(e)
 		case *streamError:
-			rt.st.stream.CloseRead()
-			rt.st.stream.Reset(uint64(e.code))
+			rt.st.CloseRead()
+			rt.st.Reset(uint64(e.code))
 		default:
-			rt.st.stream.CloseRead()
-			rt.st.stream.Reset(uint64(errH3NoError))
+			rt.st.CloseRead()
+			rt.st.Reset(uint64(errH3NoError))
 		}
 	})
 	return rt.err
@@ -88,9 +95,20 @@
 
 // RoundTrip sends a request on the connection.
 func (cc *clientConn) RoundTrip(req *http.Request) (_ *http.Response, err error) {
+	cc.mu.Lock()
+	if cc.reserved > 0 {
+		cc.reserved--
+	}
+	cc.active++
+	cc.mu.Unlock()
+
 	// Each request gets its own QUIC stream.
 	st, err := newConnStream(req.Context(), cc.qconn, streamTypeRequest)
 	if err != nil {
+		cc.mu.Lock()
+		cc.active--
+		cc.mu.Unlock()
+		cc.maybeCallStateHook()
 		return nil, err
 	}
 	rt := &roundTripState{
@@ -112,6 +130,7 @@
 	st.stream.SetReadContext(req.Context())
 	st.stream.SetWriteContext(req.Context())
 
+	addedGzip := httpcommon.IsRequestGzip(req.Method, req.Header, cc.tr.tr1.DisableCompression)
 	headers := cc.enc.encode(func(yield func(itype indexType, name, value string)) {
 		_, err = httpcommon.EncodeHeaders(req.Context(), httpcommon.EncodeHeadersParam{
 			Request: httpcommon.Request{
@@ -122,7 +141,7 @@
 				Trailer:             req.Trailer,
 				ActualContentLength: actualContentLength(req),
 			},
-			AddGzipHeader:         false, // TODO: add when appropriate
+			AddGzipHeader:         addedGzip,
 			PeerMaxHeaderListSize: 0,
 			DefaultUserAgent:      "Go-http-client/3",
 		}, func(name, value string) {
@@ -215,7 +234,13 @@
 				Trailer:       trailer,
 				Body:          (*transportResponseBody)(rt),
 			}
-			// TODO: Automatic Content-Type: gzip decoding.
+			if addedGzip && strings.EqualFold(h.Get("Content-Encoding"), "gzip") {
+				resp.Body = &gzipReader{body: resp.Body}
+				h.Del("Content-Encoding")
+				h.Del("Content-Length")
+				resp.ContentLength = -1
+				resp.Uncompressed = true
+			}
 			return resp, nil
 		case frameTypePushPromise:
 			if err := cc.handlePushPromise(st); err != nil {
@@ -290,7 +315,7 @@
 	rt.closeReqBody()
 	// Close the request stream, since we're done with the request.
 	// Reset closes the sending half of the stream.
-	rt.st.stream.Reset(uint64(errH3NoError))
+	rt.st.Reset(uint64(errH3NoError))
 	// respBody.Close is responsible for closing the receiving half.
 	err := rt.respBody.Close()
 	if err == nil {
diff --git a/internal/http3/roundtrip_test.go b/internal/http3/roundtrip_test.go
index 6c8b997..2ca08ae 100644
--- a/internal/http3/roundtrip_test.go
+++ b/internal/http3/roundtrip_test.go
@@ -6,6 +6,7 @@
 
 import (
 	"bytes"
+	"compress/gzip"
 	"errors"
 	"io"
 	"net/http"
@@ -13,6 +14,7 @@
 	"net/textproto"
 	"reflect"
 	"slices"
+	"strconv"
 	"strings"
 	"testing"
 	"testing/synctest"
@@ -29,7 +31,7 @@
 		req.Header["User-Agent"] = nil
 		rt := tc.roundTrip(req)
 		st := tc.wantStream(streamTypeRequest)
-		st.wantHeaders(http.Header{
+		st.wantSomeHeaders(http.Header{
 			":authority": []string{"example.tld"},
 			":method":    []string{"GET"},
 			":path":      []string{"/"},
@@ -427,7 +429,7 @@
 			":status": []string{"200"},
 		})
 		st.writeData(serverBody)
-		st.stream.stream.CloseWrite()
+		st.CloseWrite()
 
 		// Client receives the response from server.
 		rt.wantStatus(200)
@@ -476,7 +478,7 @@
 		st.wantIdle("client does not send its body without getting status 100")
 		serverBody := []byte("server's body")
 		st.writeData(serverBody)
-		st.stream.stream.CloseWrite()
+		st.CloseWrite()
 
 		rt.wantStatus(200)
 		rt.wantBody(serverBody)
@@ -516,7 +518,7 @@
 		st.writeHeaders(http.Header{
 			":status": {"200"},
 		})
-		st.stream.stream.CloseWrite()
+		st.CloseWrite()
 		rt.wantStatus(200)
 		st.wantClosed("request is complete")
 
@@ -557,13 +559,15 @@
 		req, _ = http.NewRequest("POST", "https://example.tld/", io.MultiReader(
 			testReader{readFunc: func(_ []byte) (int, error) {
 				req.Trailer["Client-Trailer-A"] = []string{"valuea"}
-				req.Trailer["Undeclared-Trailer"] = []string{"undeclared"} // Should be ignored.
+				// Transport should not send undeclared trailer.
+				req.Trailer["Undeclared-Trailer"] = []string{"undeclared"}
 				return 0, io.EOF
 			}},
 			strings.NewReader("a body"),
 			testReader{readFunc: func(_ []byte) (int, error) {
 				req.Trailer["Client-Trailer-B"] = []string{"valueb"}
-				req.Trailer["Undeclared-Trailer"] = []string{"undeclared"} // Should be ignored.
+				// Transport should not send undeclared trailer.
+				req.Trailer["Undeclared-Trailer"] = []string{"undeclared"}
 				return 0, io.EOF
 			}},
 		))
@@ -592,12 +596,14 @@
 		req, _ = http.NewRequest("POST", "https://example.tld/", io.MultiReader(
 			testReader{readFunc: func(_ []byte) (int, error) {
 				req.Trailer["Client-Trailer-A"] = []string{"valuea"}
-				req.Trailer["Undeclared-Trailer"] = []string{"undeclared"} // Should be ignored.
+				// Transport should not send undeclared trailer.
+				req.Trailer["Undeclared-Trailer"] = []string{"undeclared"}
 				return 0, io.EOF
 			}},
 			testReader{readFunc: func(_ []byte) (int, error) {
 				req.Trailer["Client-Trailer-B"] = []string{"valueb"}
-				req.Trailer["Undeclared-Trailer"] = []string{"undeclared"} // Should be ignored.
+				// Transport should not send undeclared trailer.
+				req.Trailer["Undeclared-Trailer"] = []string{"undeclared"}
 				return 0, io.EOF
 			}},
 		))
@@ -637,7 +643,7 @@
 			"server-trailer-a": {"valuea"},
 			// Note that Server-Trailer-B is skipped.
 			"server-trailer-c":   {"valuec"},
-			"undeclared-trailer": {"undeclared"}, // Should be ignored.
+			"undeclared-trailer": {"undeclared"},
 		})
 
 		rt.wantStatus(200)
@@ -655,6 +661,8 @@
 			"Server-Trailer-A": {"valuea"},
 			"Server-Trailer-B": nil,
 			"Server-Trailer-C": {"valuec"},
+			// Transport should accept undeclared trailers.
+			"Undeclared-Trailer": {"undeclared"},
 		})
 		st.wantClosed("request is complete")
 	})
@@ -680,7 +688,7 @@
 			"server-trailer-a": {"valuea"},
 			// Note that Server-Trailer-B is skipped.
 			"server-trailer-c":   {"valuec"},
-			"undeclared-trailer": {"undeclared"}, // Should be ignored.
+			"undeclared-trailer": {"undeclared"},
 		})
 
 		rt.wantStatus(200)
@@ -698,6 +706,8 @@
 			"Server-Trailer-A": {"valuea"},
 			"Server-Trailer-B": nil,
 			"Server-Trailer-C": {"valuec"},
+			// Transport should accept undeclared trailers.
+			"Undeclared-Trailer": {"undeclared"},
 		})
 		st.wantClosed("request is complete")
 	})
@@ -750,10 +760,257 @@
 		})
 		body := []byte("some body")
 		st.writeData(body)
-		st.stream.stream.CloseWrite()
+		st.CloseWrite()
 
 		rt.wantStatus(200)
 		rt.wantBody(body)
 		st.wantClosed("request is complete")
 	})
 }
+
+func TestRoundTripGzipEnabled(t *testing.T) {
+	tests := []struct {
+		name     string
+		explicit bool
+	}{
+		{
+			name:     "transparent",
+			explicit: false,
+		},
+		{
+			name:     "explicit",
+			explicit: true,
+		},
+	}
+	for _, tt := range tests {
+		synctestSubtest(t, tt.name, func(t *testing.T) {
+			tc := newTestClientConn(t)
+			tc.greet()
+
+			req, _ := http.NewRequest("GET", "https://example.tld/", nil)
+			if tt.explicit {
+				req.Header.Set("Accept-Encoding", "gzip")
+			}
+			rt := tc.roundTrip(req)
+			st := tc.wantStream(streamTypeRequest)
+
+			// Verify that client sends Accept-Encoding: gzip.
+			st.wantSomeHeaders(http.Header{
+				"Accept-Encoding": []string{"gzip"},
+			})
+
+			// Server responds with gzip.
+			var buf bytes.Buffer
+			gw := gzip.NewWriter(&buf)
+			gw.Write([]byte("hello world"))
+			gw.Close()
+			st.writeHeaders(http.Header{
+				":status":          []string{"200"},
+				"content-encoding": []string{"gzip"},
+				"content-length":   []string{strconv.Itoa(buf.Len())},
+			})
+			st.writeData(buf.Bytes())
+			st.CloseWrite()
+
+			rt.wantStatus(200)
+
+			if tt.explicit {
+				// When user explicitly sets gzip, the server response should
+				// be given as is.
+				rt.wantBody(buf.Bytes())
+				resp, err := rt.result()
+				if err != nil {
+					t.Fatal(err)
+				}
+				if resp.Header.Get("Content-Encoding") != "gzip" {
+					t.Errorf("Content-Encoding = %q, want gzip", resp.Header.Get("Content-Encoding"))
+				}
+				if resp.Header.Get("Content-Length") != strconv.Itoa(buf.Len()) {
+					t.Errorf("Content-Length = %q, want %d", resp.Header.Get("Content-Length"), buf.Len())
+				}
+				if resp.ContentLength != int64(buf.Len()) {
+					t.Errorf("ContentLength = %d, want %d", resp.ContentLength, buf.Len())
+				}
+				if resp.Uncompressed {
+					t.Errorf("Uncompressed = true, want false")
+				}
+			} else {
+				// When gzip is transparently set, we automatically decode the
+				// response body, and make sure stale information about the
+				// gzip content length and encoding are updated.
+				rt.wantBody([]byte("hello world"))
+				resp, err := rt.result()
+				if err != nil {
+					t.Fatal(err)
+				}
+				if resp.Header.Get("Content-Encoding") != "" {
+					t.Errorf("Content-Encoding = %q, want empty", resp.Header.Get("Content-Encoding"))
+				}
+				if resp.Header.Get("Content-Length") != "" {
+					t.Errorf("Content-Length = %q, want empty", resp.Header.Get("Content-Length"))
+				}
+				if resp.ContentLength != -1 {
+					t.Errorf("ContentLength = %d, want -1", resp.ContentLength)
+				}
+				if !resp.Uncompressed {
+					t.Errorf("Uncompressed = false, want true")
+				}
+			}
+		})
+	}
+}
+
+func TestRoundTripGzipDisabled(t *testing.T) {
+	tests := []struct {
+		name  string
+		setup func(tc *testClientConn, req *http.Request, wantHeaders http.Header)
+	}{
+		{
+			name: "explicitly disabled",
+			setup: func(tc *testClientConn, req *http.Request, wantHeaders http.Header) {
+				tc.tr.tr1.DisableCompression = true
+			},
+		},
+		{
+			name: "HEAD request",
+			setup: func(tc *testClientConn, req *http.Request, wantHeaders http.Header) {
+				req.Method = "HEAD"
+				wantHeaders.Set(":method", "HEAD")
+			},
+		},
+		{
+			name: "contains Range header",
+			setup: func(tc *testClientConn, req *http.Request, wantHeaders http.Header) {
+				req.Header.Set("Range", "bytes=0-10")
+				wantHeaders.Set("Range", "bytes=0-10")
+			},
+		},
+		{
+			name: "contains Accept-Encoding-identity header",
+			setup: func(tc *testClientConn, req *http.Request, wantHeaders http.Header) {
+				req.Header.Set("Accept-Encoding", "identity")
+				wantHeaders.Set("Accept-Encoding", "identity")
+			},
+		},
+	}
+	for _, tt := range tests {
+		synctestSubtest(t, tt.name, func(t *testing.T) {
+			tc := newTestClientConn(t)
+			req, _ := http.NewRequest("GET", "https://example.tld/", nil)
+			wantHeaders := http.Header{
+				":authority": []string{"example.tld"},
+				":method":    []string{"GET"},
+				":path":      []string{"/"},
+				":scheme":    []string{"https"},
+				"User-Agent": []string{"Go-http-client/3"},
+			}
+			tt.setup(tc, req, wantHeaders)
+			tc.greet()
+
+			rt := tc.roundTrip(req)
+			st := tc.wantStream(streamTypeRequest)
+
+			// Verify that client does not send Accept-Encoding: gzip.
+			st.wantHeaders(wantHeaders)
+
+			st.writeHeaders(http.Header{
+				":status": []string{"200"},
+			})
+			rt.wantStatus(200)
+		})
+	}
+}
+
+func TestRoundTripGzipWithTrailers(t *testing.T) {
+	synctest.Test(t, func(t *testing.T) {
+		tc := newTestClientConn(t)
+		tc.greet()
+
+		req, _ := http.NewRequest("GET", "https://example.tld/", nil)
+		rt := tc.roundTrip(req)
+		st := tc.wantStream(streamTypeRequest)
+
+		// Verify that client sends Accept-Encoding: gzip.
+		st.wantSomeHeaders(http.Header{
+			"Accept-Encoding": []string{"gzip"},
+		})
+
+		// Server responds with gzip and trailer declaration.
+		var buf bytes.Buffer
+		gw := gzip.NewWriter(&buf)
+		gw.Write([]byte("hello world"))
+		gw.Close()
+		st.writeHeaders(http.Header{
+			":status":          []string{"200"},
+			"content-encoding": []string{"gzip"},
+			"trailer":          []string{"Server-Trailer-A"},
+		})
+		st.writeData(buf.Bytes())
+		st.writeHeaders(http.Header{
+			"server-trailer-a": {"valuea"},
+		})
+		st.CloseWrite()
+
+		rt.wantStatus(200)
+		rt.wantTrailers(http.Header{
+			"Server-Trailer-A": nil,
+		})
+		rt.wantBody([]byte("hello world"))
+		rt.wantTrailers(http.Header{
+			"Server-Trailer-A": {"valuea"},
+		})
+		st.wantClosed("request is complete")
+	})
+}
+
+func TestRoundTripGzipConcurrentCloseAndRead(t *testing.T) {
+	synctest.Test(t, func(t *testing.T) {
+		tc := newTestClientConn(t)
+		tc.greet()
+
+		req, _ := http.NewRequest("GET", "https://example.tld/", nil)
+		rt := tc.roundTrip(req)
+		st := tc.wantStream(streamTypeRequest)
+
+		// Verify that client sends Accept-Encoding: gzip.
+		st.wantSomeHeaders(http.Header{
+			"Accept-Encoding": []string{"gzip"},
+		})
+
+		// Server responds with gzip.
+		st.writeHeaders(http.Header{
+			":status":          []string{"200"},
+			"content-encoding": []string{"gzip"},
+		})
+		rt.wantStatus(200)
+
+		resp, err := rt.result()
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		// Read from the response body in a goroutine while it is empty.
+		// This will block indefinitely.
+		readErrCh := make(chan error, 1)
+		go func() {
+			var p [10]byte
+			_, err := resp.Body.Read(p[:])
+			readErrCh <- err
+		}()
+		synctest.Wait()
+
+		if err := resp.Body.Close(); err != nil {
+			t.Fatalf("Body.Close() = %v", err)
+		}
+		synctest.Wait()
+
+		select {
+		case err := <-readErrCh:
+			if err == nil {
+				t.Error("Read returned nil error, want error")
+			}
+		default:
+			t.Error("Read did not unblock on Close")
+		}
+	})
+}
diff --git a/internal/http3/server.go b/internal/http3/server.go
index 14b510f..999b3f1 100644
--- a/internal/http3/server.go
+++ b/internal/http3/server.go
@@ -7,10 +7,13 @@
 import (
 	"context"
 	"crypto/tls"
+	"errors"
 	"fmt"
 	"io"
 	"maps"
 	"net/http"
+	"net/textproto"
+	"os"
 	"slices"
 	"strconv"
 	"strings"
@@ -503,9 +506,10 @@
 	}
 	if contentLength != 0 || len(reqInfo.Trailer) != 0 {
 		body = &bodyReader{
-			st:      st,
-			remain:  contentLength,
-			trailer: reqInfo.Trailer,
+			st:            st,
+			remain:        contentLength,
+			trailer:       reqInfo.Trailer,
+			filterTrailer: true,
 		}
 	} else {
 		body = http.NoBody
@@ -578,11 +582,18 @@
 	return true
 }
 
+// trailerPrefix is a magic prefix for [responseWriter.Header] map keys that,
+// if present, signals that the map entry is actually for the response
+// trailers, and not the response headers. See [net/http.TrailerPrefix] for
+// details.
+const trailerPrefix = "Trailer:"
+
 type responseWriter struct {
 	st             *stream
 	bw             *bodyWriter
 	mu             sync.Mutex
 	headers        http.Header
+	snapHeaders    http.Header // Snapshot of headers at WriteHeader time
 	trailer        http.Header
 	bb             bodyBuffer
 	wroteHeader    bool // Non-1xx header has been (logically) written.
@@ -607,6 +618,12 @@
 			delete(rw.trailer, name)
 		}
 	}
+	for name, vals := range rw.headers {
+		if name, found := strings.CutPrefix(name, trailerPrefix); found {
+			name = textproto.CanonicalMIMEHeaderKey(textproto.TrimString(name))
+			rw.trailer[name] = vals
+		}
+	}
 	if len(rw.trailer) > 0 {
 		rw.bw.trailer = rw.trailer
 	}
@@ -623,18 +640,18 @@
 	if !responseCanHaveBody(rw.statusCode) {
 		rw.cannotHaveBody = true
 	}
-	// If there is any Trailer declared in headers, save them so we know which
-	// trailers have been pre-declared. Also, write back the extracted value,
-	// which is canonicalized, to rw.Header for consistency.
-	if _, ok := rw.headers["Trailer"]; ok {
-		extractTrailerFromHeader(rw.headers, rw.trailer)
-		rw.headers.Set("Trailer", strings.Join(slices.Sorted(maps.Keys(rw.trailer)), ", "))
+	// If there is any Trailer declared, save them so we know which trailers
+	// have been pre-declared. Also, write back the extracted value, which is
+	// canonicalized, for consistency.
+	if _, ok := rw.snapHeaders["Trailer"]; ok {
+		extractTrailerFromHeader(rw.snapHeaders, rw.trailer)
+		rw.snapHeaders.Set("Trailer", strings.Join(slices.Sorted(maps.Keys(rw.trailer)), ", "))
 	}
 
-	rw.bb.inferHeader(rw.headers, rw.statusCode)
+	rw.bb.inferHeader(rw.snapHeaders, rw.statusCode)
 	encHeaders := rw.bw.enc.encode(func(f func(itype indexType, name, value string)) {
 		f(mayIndex, ":status", strconv.Itoa(rw.statusCode))
-		for name, values := range rw.headers {
+		for name, values := range rw.snapHeaders {
 			if !httpguts.ValidHeaderFieldName(name) {
 				continue
 			}
@@ -727,6 +744,7 @@
 	// buffered.
 	rw.statusCodeSet = true
 	rw.statusCode = statusCode
+	rw.snapHeaders = rw.headers.Clone()
 	if n, err := strconv.Atoi(rw.Header().Get("Content-Length")); err == nil {
 		rw.bodyLenLeft = n
 	} else {
@@ -796,7 +814,22 @@
 	return initialBLen, nil
 }
 
-func (rw *responseWriter) Flush() {
+func (rw *responseWriter) SetReadDeadline(deadline time.Time) error {
+	rw.st.readDeadline.set(deadline)
+	return nil
+}
+
+func (rw *responseWriter) SetWriteDeadline(deadline time.Time) error {
+	rw.st.writeDeadline.set(deadline)
+	return nil
+}
+
+func (rw *responseWriter) EnableFullDuplex() error {
+	return nil
+}
+
+func (rw *responseWriter) Flush() { rw.FlushError() }
+func (rw *responseWriter) FlushError() error {
 	// Calling Flush implicitly calls WriteHeader(200) if WriteHeader has not
 	// been called before.
 	rw.WriteHeader(http.StatusOK)
@@ -804,21 +837,31 @@
 	defer rw.mu.Unlock()
 	rw.writeHeaderLockedOnce()
 	if !rw.cannotHaveBody {
-		rw.bw.Write(rw.bb)
+		_, err := rw.bw.Write(rw.bb)
 		rw.bb.discard()
+		if err != nil {
+			return err
+		}
 	}
-	rw.st.Flush()
+	return rw.st.Flush()
 }
 
 func (rw *responseWriter) close() error {
-	rw.Flush()
+	retErr := rw.FlushError()
 	rw.mu.Lock()
 	defer rw.mu.Unlock()
+
 	rw.prepareTrailerForWriteLocked()
-	if err := rw.bw.Close(); err != nil {
-		return err
+	if err := rw.bw.Close(); retErr == nil {
+		retErr = err
 	}
-	return rw.st.stream.Close()
+
+	if errors.Is(rw.st.writeDeadline.err(), os.ErrDeadlineExceeded) {
+		rw.st.Reset(uint64(errH3RequestCancelled))
+	} else if err := rw.st.Close(); retErr == nil {
+		retErr = err
+	}
+	return retErr
 }
 
 // defaultBodyBufferCap is the default number of bytes of body that we are
diff --git a/internal/http3/server_test.go b/internal/http3/server_test.go
index 5210d6d..4908471 100644
--- a/internal/http3/server_test.go
+++ b/internal/http3/server_test.go
@@ -12,6 +12,7 @@
 	"net/http"
 	"net/netip"
 	"net/url"
+	"os"
 	"reflect"
 	"slices"
 	"strconv"
@@ -93,6 +94,28 @@
 	})
 }
 
+func TestServerHeaderSnapshot(t *testing.T) {
+	synctest.Test(t, func(t *testing.T) {
+		ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			w.Header().Set("X-Test-Header", "original")
+			w.WriteHeader(200)
+			w.Header().Set("X-Test-Header", "modified")
+			w.Write([]byte("body"))
+		}))
+		tc := ts.connect()
+		tc.greet()
+
+		reqStream := tc.newStream(streamTypeRequest)
+		reqStream.writeHeaders(requestHeader(nil))
+		reqStream.wantSomeHeaders(http.Header{
+			":status":       {"200"},
+			"X-Test-Header": {"original"},
+		})
+		reqStream.wantData([]byte("body"))
+		reqStream.wantClosed("request is complete")
+	})
+}
+
 func TestServerHeaderInvalid(t *testing.T) {
 	tests := []struct {
 		name      string
@@ -406,7 +429,7 @@
 		reqStream.writeHeaders(requestHeader(nil))
 		bodyContent := []byte("some body content that should be echoed")
 		reqStream.writeData(bodyContent)
-		reqStream.stream.stream.CloseWrite()
+		reqStream.CloseWrite()
 		reqStream.wantSomeHeaders(http.Header{":status": {"200"}})
 		// Small multiple calls to Write will be coalesced into one DATA frame.
 		reqStream.wantData(append([]byte("/"), bodyContent...))
@@ -649,7 +672,7 @@
 		reqStream.wantSomeHeaders(http.Header{":status": {"100"}})
 		body := []byte("body that will be echoed back if we get status 100")
 		reqStream.writeData(body)
-		reqStream.stream.stream.CloseWrite()
+		reqStream.CloseWrite()
 
 		// Receive the server's response after sending the body.
 		reqStream.wantSomeHeaders(http.Header{":status": {"200"}})
@@ -703,7 +726,7 @@
 		// read the request body without hanging, which would normally cause an
 		// HTTP 100 to be sent.
 		reqStream.writeData([]byte("some body"))
-		reqStream.stream.stream.CloseWrite()
+		reqStream.CloseWrite()
 
 		// Verify that no HTTP 100 was sent.
 		reqStream.wantSomeHeaders(http.Header{":status": {"200"}})
@@ -727,7 +750,7 @@
 		// client closes the write direction of the stream.
 		reqStream := tc.newStream(streamTypeRequest)
 		reqStream.writeHeaders(requestHeader(nil))
-		reqStream.stream.stream.CloseWrite()
+		reqStream.CloseWrite()
 		reqStream.wantSomeHeaders(http.Header{":status": {"200"}})
 		reqStream.wantData(serverBody)
 		reqStream.wantClosed("request is complete")
@@ -776,9 +799,10 @@
 		}))
 		reqStream.writeData(body)
 		reqStream.writeHeaders(http.Header{
-			"Client-Trailer-A":   {"valuea"},
-			"Client-Trailer-B":   {"valueb"},
-			"Undeclared-Trailer": {"undeclared"}, // Undeclared trailer should be ignored.
+			"Client-Trailer-A": {"valuea"},
+			"Client-Trailer-B": {"valueb"},
+			// Server should not accept undeclared trailers.
+			"Undeclared-Trailer": {"undeclared"},
 		})
 		reqStream.wantHeaders(nil)
 		reqStream.wantClosed("request is complete")
@@ -816,9 +840,10 @@
 			"content-length": {"0"},
 		}))
 		reqStream.writeHeaders(http.Header{
-			"Client-Trailer-A":   {"valuea"},
-			"Client-Trailer-B":   {"valueb"},
-			"Undeclared-Trailer": {"undeclared"}, // Undeclared trailer should be ignored.
+			"Client-Trailer-A": {"valuea"},
+			"Client-Trailer-B": {"valueb"},
+			// Server should not accept undeclared trailers.
+			"Undeclared-Trailer": {"undeclared"},
 		})
 		reqStream.wantHeaders(nil)
 		reqStream.wantClosed("request is complete")
@@ -836,7 +861,10 @@
 
 			w.Header().Set("server-trailer-a", "valuea") // Trailer header will be canonicalized.
 			w.Header().Set("Server-Trailer-C", "valuec") // skipping B
+			// Server should not send undeclared trailers, unless it has the
+			// magic "Trailer:" prefix.
 			w.Header().Set("Server-Trailer-Not-Declared", "should be omitted")
+			w.Header().Set("Trailer:Undeclared-Trailer-Exception", "should be sent")
 		}))
 		tc := ts.connect()
 		tc.greet()
@@ -849,8 +877,9 @@
 		})
 		reqStream.wantData(body)
 		reqStream.wantSomeHeaders(http.Header{
-			"Server-Trailer-A": {"valuea"},
-			"Server-Trailer-C": {"valuec"},
+			"Server-Trailer-A":             {"valuea"},
+			"Server-Trailer-C":             {"valuec"},
+			"Undeclared-Trailer-Exception": {"should be sent"},
 		})
 		reqStream.wantClosed("request is complete")
 	})
@@ -866,7 +895,10 @@
 
 			w.Header().Set("server-trailer-a", "valuea") // Trailer header will be canonicalized.
 			w.Header().Set("Server-Trailer-C", "valuec") // skipping B
+			// Server should not send undeclared trailers without "Trailer:"
+			// prefix.
 			w.Header().Set("Server-Trailer-Not-Declared", "should be omitted")
+			w.Header().Set("Trailer:undeclared-trailer-exception", "should be sent")
 		}))
 		tc := ts.connect()
 		tc.greet()
@@ -878,8 +910,9 @@
 			"Trailer": {"Server-Trailer-A, Server-Trailer-B, Server-Trailer-C"},
 		})
 		reqStream.wantSomeHeaders(http.Header{
-			"Server-Trailer-A": {"valuea"},
-			"Server-Trailer-C": {"valuec"},
+			"Server-Trailer-A":             {"valuea"},
+			"Server-Trailer-C":             {"valuec"},
+			"Undeclared-Trailer-Exception": {"should be sent"},
 		})
 		reqStream.wantClosed("request is complete")
 	})
@@ -1202,6 +1235,220 @@
 	})
 }
 
+func TestServerPastWriteDeadline(t *testing.T) {
+	synctest.Test(t, func(t *testing.T) {
+		ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			ctl := http.NewResponseController(w)
+			io.WriteString(w, "one")
+			if err := ctl.Flush(); err != nil {
+				t.Errorf("Flush() = %v, want nil", err)
+			}
+			time.Sleep(time.Second) // T+1.
+			// Set past write deadline. Write should fail.
+			if err := ctl.SetWriteDeadline(time.Now().Add(-10 * time.Second)); err != nil {
+				t.Errorf("SetWriteDeadline() = %v, want nil", err)
+			}
+			var err error
+			_, err = io.WriteString(w, "x")
+			if err == nil {
+				err = ctl.Flush()
+			}
+			if !errors.Is(err, os.ErrDeadlineExceeded) {
+				t.Errorf("got write err %v, want %v", err, os.ErrDeadlineExceeded)
+			}
+
+			// Extending the write deadline after it's exceeded should have no effect (sticky).
+			if err := ctl.SetWriteDeadline(time.Now().Add(10 * time.Second)); err != nil {
+				t.Errorf("SetWriteDeadline() = %v, want nil", err)
+			}
+			_, err = io.WriteString(w, "x")
+			if err == nil {
+				err = ctl.Flush()
+			}
+			if !errors.Is(err, os.ErrDeadlineExceeded) {
+				t.Errorf("got write err %v (after extend), want %v", err, os.ErrDeadlineExceeded)
+			}
+		}))
+		tc := ts.connect()
+		tc.greet()
+
+		reqStream := tc.newStream(streamTypeRequest)
+		reqStream.writeHeaders(requestHeader(nil))
+		reqStream.wantSomeHeaders(http.Header{":status": {"200"}})
+		reqStream.wantData([]byte("one"))
+		time.Sleep(2 * time.Second) // T+2.
+		synctest.Wait()
+		reqStream.wantError(quic.StreamErrorCode(errH3RequestCancelled))
+	})
+}
+
+func TestServerFutureWriteDeadline(t *testing.T) {
+	synctest.Test(t, func(t *testing.T) {
+		ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			ctl := http.NewResponseController(w)
+			io.WriteString(w, "one")
+			if err := ctl.Flush(); err != nil {
+				t.Errorf("Flush() = %v, want nil", err)
+			}
+
+			// Set future deadline at T+1. Write should succeed.
+			if err := ctl.SetWriteDeadline(time.Now().Add(time.Second)); err != nil {
+				t.Errorf("SetWriteDeadline() = %v, want nil", err)
+			}
+			io.WriteString(w, "two")
+			if err := ctl.Flush(); err != nil {
+				t.Errorf("Flush() = %v, want nil", err)
+			}
+
+			// Extend deadline to T+3, before it expires.
+			if err := ctl.SetWriteDeadline(time.Now().Add(3 * time.Second)); err != nil {
+				t.Errorf("SetWriteDeadline() = %v, want nil", err)
+			}
+			// Sleep till T+2. Write should succeed since the deadline is T+3.
+			time.Sleep(2 * time.Second)
+			io.WriteString(w, "three")
+			if err := ctl.Flush(); err != nil {
+				t.Errorf("Flush() = %v, want nil", err)
+			}
+
+			// Sleep till T+4. Write should fail since deadline is T+3.
+			time.Sleep(2 * time.Second)
+			var err error
+			_, err = io.WriteString(w, "x")
+			if err == nil {
+				err = ctl.Flush()
+			}
+			if !errors.Is(err, os.ErrDeadlineExceeded) {
+				t.Errorf("got write err %v, want %v", err, os.ErrDeadlineExceeded)
+			}
+
+			// Extending the write deadline after it's exceeded should have no effect (sticky).
+			if err := ctl.SetWriteDeadline(time.Time{}); err != nil {
+				t.Errorf("SetWriteDeadline() = %v, want nil", err)
+			}
+			_, err = io.WriteString(w, "x")
+			if err == nil {
+				err = ctl.Flush()
+			}
+			if !errors.Is(err, os.ErrDeadlineExceeded) {
+				t.Errorf("got write err %v (after extend), want %v", err, os.ErrDeadlineExceeded)
+			}
+		}))
+		tc := ts.connect()
+		tc.greet()
+
+		reqStream := tc.newStream(streamTypeRequest)
+		reqStream.writeHeaders(requestHeader(nil))
+		reqStream.wantSomeHeaders(http.Header{":status": {"200"}})
+		reqStream.wantData([]byte("one"))
+		reqStream.wantData([]byte("two"))
+		time.Sleep(3 * time.Second) // T+3. After "three" is written.
+		reqStream.wantData([]byte("three"))
+		time.Sleep(3 * time.Second) // T+6. After server exceeds deadline.
+		reqStream.wantError(quic.StreamErrorCode(errH3RequestCancelled))
+	})
+}
+
+func TestServerPastReadDeadline(t *testing.T) {
+	synctest.Test(t, func(t *testing.T) {
+		ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			ctl := http.NewResponseController(w)
+			b := make([]byte, 3)
+			if _, err := io.ReadFull(r.Body, b); err != nil || string(b) != "one" {
+				t.Errorf("Read() got (%q, %v), want (%q, nil)", b, err, "one")
+			}
+			// Set past read deadline. Read should fail.
+			if err := ctl.SetReadDeadline(time.Now().Add(-10 * time.Second)); err != nil {
+				t.Errorf("SetReadDeadline() = %v, want nil", err)
+			}
+			_, err := io.ReadAll(r.Body)
+			if !errors.Is(err, os.ErrDeadlineExceeded) {
+				t.Errorf("got read err %v, want %v", err, os.ErrDeadlineExceeded)
+			}
+
+			// Extending the read deadline after it's exceeded should have no effect (sticky).
+			if err := ctl.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil {
+				t.Errorf("SetReadDeadline() = %v, want nil", err)
+			}
+			_, err = io.ReadAll(r.Body)
+			if !errors.Is(err, os.ErrDeadlineExceeded) {
+				t.Errorf("got read err %v (after extend), want %v", err, os.ErrDeadlineExceeded)
+			}
+		}))
+		tc := ts.connect()
+		tc.greet()
+
+		reqStream := tc.newStream(streamTypeRequest)
+		reqStream.writeHeaders(requestHeader(nil))
+		reqStream.writeData([]byte("one"))
+		synctest.Wait()
+	})
+}
+
+func TestServerFutureReadDeadline(t *testing.T) {
+	synctest.Test(t, func(t *testing.T) {
+		ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			ctl := http.NewResponseController(w)
+			b := make([]byte, 3)
+			if _, err := io.ReadFull(r.Body, b); err != nil || string(b) != "one" {
+				t.Errorf("Read() got (%q, %v), want (%q, nil)", b, err, "one")
+			}
+
+			// Set future deadline at T+2s. Read should succeed.
+			if err := ctl.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
+				t.Errorf("SetReadDeadline() = %v, want nil", err)
+			}
+			b2 := make([]byte, 3)
+			if _, err := io.ReadFull(r.Body, b2); err != nil || string(b2) != "two" {
+				t.Errorf("Read() got (%q, %v), want (%q, nil)", b2, err, "two")
+			}
+
+			// Extend deadline to T+5s, before it expires.
+			if err := ctl.SetReadDeadline(time.Now().Add(4 * time.Second)); err != nil {
+				t.Errorf("SetReadDeadline() = %v, want nil", err)
+			}
+			// Sleep till T+3. Read should succeed since the deadline is T+5.
+			time.Sleep(2 * time.Second)
+			b3 := make([]byte, 5)
+			if _, err := io.ReadFull(r.Body, b3); err != nil || string(b3) != "three" {
+				t.Errorf("Read() got (%q, %v), want (%q, nil)", b3, err, "three")
+			}
+
+			// Sleep till T+6. Read should fail since deadline has passed.
+			time.Sleep(3 * time.Second)
+			_, err := io.ReadAll(r.Body)
+			if !errors.Is(err, os.ErrDeadlineExceeded) {
+				t.Errorf("got read err %v, want %v", err, os.ErrDeadlineExceeded)
+			}
+
+			// Extending the read deadline after it's exceeded should have no effect (sticky).
+			if err := ctl.SetReadDeadline(time.Time{}); err != nil {
+				t.Errorf("SetReadDeadline() = %v, want nil", err)
+			}
+			_, err = io.ReadAll(r.Body)
+			if !errors.Is(err, os.ErrDeadlineExceeded) {
+				t.Errorf("got read err %v (after extend), want %v", err, os.ErrDeadlineExceeded)
+			}
+		}))
+		tc := ts.connect()
+		tc.greet()
+
+		reqStream := tc.newStream(streamTypeRequest)
+		reqStream.writeHeaders(requestHeader(nil))
+		reqStream.writeData([]byte("one"))
+
+		time.Sleep(time.Second)
+		reqStream.writeData([]byte("two")) // T+1.
+		synctest.Wait()
+
+		time.Sleep(time.Second)
+		reqStream.writeData([]byte("three")) // T+2.
+		synctest.Wait()
+
+		time.Sleep(4 * time.Second) // Advance to T+6 for server handler to complete.
+	})
+}
+
 type testServer struct {
 	t  testing.TB
 	s  *server
diff --git a/internal/http3/stream.go b/internal/http3/stream.go
index 93294d4..3529983 100644
--- a/internal/http3/stream.go
+++ b/internal/http3/stream.go
@@ -7,6 +7,9 @@
 import (
 	"context"
 	"io"
+	"os"
+	"sync"
+	"time"
 
 	"golang.org/x/net/quic"
 )
@@ -21,6 +24,9 @@
 	// results in an error.
 	// -1 indicates no limit.
 	lim int64
+
+	readDeadline  deadline
+	writeDeadline deadline
 }
 
 // newConnStream creates a new stream on a connection.
@@ -42,10 +48,7 @@
 	if err != nil {
 		return nil, err
 	}
-	st := &stream{
-		stream: qs,
-		lim:    -1, // no limit
-	}
+	st := newStream(qs)
 	if stype != streamTypeRequest {
 		// Unidirectional stream header.
 		st.writeVarint(int64(stype))
@@ -54,9 +57,121 @@
 }
 
 func newStream(qs *quic.Stream) *stream {
-	return &stream{
+	readCtx, readCancel := context.WithCancelCause(context.Background())
+	writeCtx, writeCancel := context.WithCancelCause(context.Background())
+	st := &stream{
 		stream: qs,
 		lim:    -1, // no limit
+		readDeadline: deadline{
+			ctx:    readCtx,
+			cancel: readCancel,
+		},
+		writeDeadline: deadline{
+			ctx:    writeCtx,
+			cancel: writeCancel,
+		},
+	}
+	qs.SetReadContext(readCtx)
+	qs.SetWriteContext(writeCtx)
+	return st
+}
+
+func (st *stream) Close() error {
+	st.readDeadline.stop()
+	st.writeDeadline.stop()
+	return st.stream.Close()
+}
+
+func (st *stream) CloseRead() {
+	st.readDeadline.stop()
+	st.stream.CloseRead()
+}
+
+func (st *stream) CloseWrite() {
+	st.writeDeadline.stop()
+	st.stream.CloseWrite()
+}
+
+func (st *stream) Reset(code uint64) {
+	st.readDeadline.stop()
+	st.writeDeadline.stop()
+	st.stream.Reset(code)
+}
+
+// deadline manages ctx, and cancels it when timer expires, with
+// [os.ErrDeadlineExceeded] as the cause. If the deadline is manually stopped
+// before timer expires, the context will be canceled with [context.Canceled]
+// as the cause. Once a deadline is exceeded, its timer can no longer be
+// extended.
+// Practically, this lets the http3 package support time-based deadlines by
+// utilizing the quic package's support for context-based deadlines.
+type deadline struct {
+	ctx    context.Context
+	cancel context.CancelCauseFunc
+
+	mu    sync.Mutex // Guards below.
+	timer *time.Timer
+}
+
+// stopTimerLocked stops the deadline timer and sets it to nil.
+// The caller must hold d.mu.
+func (d *deadline) stopTimerLocked() {
+	if d.timer != nil {
+		d.timer.Stop()
+		d.timer = nil
+	}
+}
+
+// stop stops the deadline timer and cancels the context with
+// [context.Canceled] as the cause.
+func (d *deadline) stop() {
+	d.mu.Lock()
+	d.stopTimerLocked()
+	d.mu.Unlock()
+	d.cancel(context.Canceled)
+}
+
+// err returns the deadline's context cancelation cause, if any.
+func (d *deadline) err() error {
+	return context.Cause(d.ctx)
+}
+
+// errOf returns the deadline's context cancelation cause if the given err is
+// non-nil. This can be used to check whether an error value returned by I/O
+// operations at the QUIC layer is non-nil because the deadline has expired.
+func (d *deadline) errOf(err error) error {
+	if dErr := d.err(); err != nil && dErr != nil {
+		return dErr
+	}
+	return err
+}
+
+// set configures a new deadline using the given deadlineTime.
+// Once deadline is exceeded, it remains in the expired (sticky) state, and
+// subsequent attempts to extend or reset the deadline are ignored.
+func (d *deadline) set(deadlineTime time.Time) {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+
+	if d.ctx.Err() != nil { // Already expired, sticky error.
+		return
+	}
+	if deadlineTime.IsZero() {
+		d.stopTimerLocked()
+		return
+	}
+	dur := time.Until(deadlineTime)
+	if dur <= 0 {
+		d.stopTimerLocked()
+		d.cancel(os.ErrDeadlineExceeded)
+		return
+	}
+	if d.timer == nil {
+		d.timer = time.AfterFunc(dur, func() {
+			d.cancel(os.ErrDeadlineExceeded)
+		})
+	} else {
+		d.timer.Reset(dur)
 	}
 }
 
@@ -110,21 +225,36 @@
 
 // ReadByte reads one byte from the stream.
 func (st *stream) ReadByte() (b byte, err error) {
+	// Check the deadline before doing I/O operations on the QUIC layer. We do
+	// this because the QUIC layer implements a fast path for I/O operations,
+	// allowing Read & Write to succeed depending on the state of buffer, even
+	// if its context has been canceled. By always checking the deadline here,
+	// we make it so that I/O operations fail as soon as its relevant deadline
+	// has been exceeded.
+	if err := st.readDeadline.err(); err != nil {
+		return 0, err
+	}
 	if err := st.recordBytesRead(1); err != nil {
 		return 0, err
 	}
 	b, err = st.stream.ReadByte()
-	if err != nil {
-		if err == io.EOF && st.lim < 0 {
-			return 0, io.EOF
-		}
+	if err == io.EOF && st.lim >= 0 {
 		return 0, errH3FrameError
 	}
-	return b, nil
+	return b, st.readDeadline.errOf(err)
 }
 
 // Read reads from the stream.
 func (st *stream) Read(b []byte) (int, error) {
+	// Check the deadline before doing I/O operations on the QUIC layer. We do
+	// this because the QUIC layer implements a fast path for I/O operations,
+	// allowing Read & Write to succeed depending on the state of buffer, even
+	// if its context has been canceled. By always checking the deadline here,
+	// we make it so that I/O operations fail as soon as its relevant deadline
+	// has been exceeded.
+	if err := st.readDeadline.err(); err != nil {
+		return 0, err
+	}
 	n, err := st.stream.Read(b)
 	if e2 := st.recordBytesRead(n); e2 != nil {
 		return 0, e2
@@ -141,10 +271,7 @@
 			return n, io.EOF
 		}
 	}
-	if err != nil {
-		return 0, errH3FrameError
-	}
-	return n, nil
+	return n, st.readDeadline.errOf(err)
 }
 
 // discardUnknownFrame discards an unknown frame.
@@ -173,7 +300,7 @@
 func (st *stream) discardFrame() error {
 	// TODO: Consider adding a *quic.Stream method to discard some amount of data.
 	for range st.lim {
-		_, err := st.stream.ReadByte()
+		_, err := st.ReadByte()
 		if err != nil {
 			return &streamError{errH3FrameError, err.Error()}
 		}
@@ -183,29 +310,66 @@
 }
 
 // Write writes to the stream.
-func (st *stream) Write(b []byte) (int, error) { return st.stream.Write(b) }
+func (st *stream) Write(b []byte) (int, error) {
+	// Check the deadline before doing I/O operations on the QUIC layer. We do
+	// this because the QUIC layer implements a fast path for I/O operations,
+	// allowing Read & Write to succeed depending on the state of buffer, even
+	// if its context has been canceled. By always checking the deadline here,
+	// we make it so that I/O operations fail as soon as its relevant deadline
+	// has been exceeded.
+	if err := st.writeDeadline.err(); err != nil {
+		return 0, err
+	}
+	n, err := st.stream.Write(b)
+	return n, st.writeDeadline.errOf(err)
+}
 
 // Flush commits data written to the stream.
-func (st *stream) Flush() error { return st.stream.Flush() }
+func (st *stream) Flush() error {
+	// Check the deadline before doing I/O operations on the QUIC layer. We do
+	// this because the QUIC layer implements a fast path for I/O operations,
+	// allowing Read & Write to succeed depending on the state of buffer, even
+	// if its context has been canceled. By always checking the deadline here,
+	// we make it so that I/O operations fail as soon as its relevant deadline
+	// has been exceeded.
+	if err := st.writeDeadline.err(); err != nil {
+		return err
+	}
+	return st.writeDeadline.errOf(st.stream.Flush())
+}
+
+// WriteByte writes one byte to the stream.
+func (st *stream) WriteByte(c byte) error {
+	// Check the deadline before doing I/O operations on the QUIC layer. We do
+	// this because the QUIC layer implements a fast path for I/O operations,
+	// allowing Read & Write to succeed depending on the state of buffer, even
+	// if its context has been canceled. By always checking the deadline here,
+	// we make it so that I/O operations fail as soon as its relevant deadline
+	// has been exceeded.
+	if err := st.writeDeadline.err(); err != nil {
+		return err
+	}
+	return st.writeDeadline.errOf(st.stream.WriteByte(c))
+}
 
 // readVarint reads a QUIC variable-length integer from the stream.
 func (st *stream) readVarint() (v int64, err error) {
-	b, err := st.stream.ReadByte()
+	b, err := st.ReadByte()
 	if err != nil {
 		return 0, err
 	}
 	v = int64(b & 0x3f)
 	n := 1 << (b >> 6)
 	for i := 1; i < n; i++ {
-		b, err := st.stream.ReadByte()
+		b, err := st.ReadByte()
 		if err != nil {
-			return 0, errH3FrameError
+			if err == io.EOF {
+				return 0, errH3FrameError
+			}
+			return 0, err
 		}
 		v = (v << 8) | int64(b)
 	}
-	if err := st.recordBytesRead(n); err != nil {
-		return 0, err
-	}
 	return v, nil
 }
 
@@ -219,24 +383,24 @@
 func (st *stream) writeVarint(v int64) {
 	switch {
 	case v <= (1<<6)-1:
-		st.stream.WriteByte(byte(v))
+		st.WriteByte(byte(v))
 	case v <= (1<<14)-1:
-		st.stream.WriteByte((1 << 6) | byte(v>>8))
-		st.stream.WriteByte(byte(v))
+		st.WriteByte((1 << 6) | byte(v>>8))
+		st.WriteByte(byte(v))
 	case v <= (1<<30)-1:
-		st.stream.WriteByte((2 << 6) | byte(v>>24))
-		st.stream.WriteByte(byte(v >> 16))
-		st.stream.WriteByte(byte(v >> 8))
-		st.stream.WriteByte(byte(v))
+		st.WriteByte((2 << 6) | byte(v>>24))
+		st.WriteByte(byte(v >> 16))
+		st.WriteByte(byte(v >> 8))
+		st.WriteByte(byte(v))
 	case v <= (1<<62)-1:
-		st.stream.WriteByte((3 << 6) | byte(v>>56))
-		st.stream.WriteByte(byte(v >> 48))
-		st.stream.WriteByte(byte(v >> 40))
-		st.stream.WriteByte(byte(v >> 32))
-		st.stream.WriteByte(byte(v >> 24))
-		st.stream.WriteByte(byte(v >> 16))
-		st.stream.WriteByte(byte(v >> 8))
-		st.stream.WriteByte(byte(v))
+		st.WriteByte((3 << 6) | byte(v>>56))
+		st.WriteByte(byte(v >> 48))
+		st.WriteByte(byte(v >> 40))
+		st.WriteByte(byte(v >> 32))
+		st.WriteByte(byte(v >> 24))
+		st.WriteByte(byte(v >> 16))
+		st.WriteByte(byte(v >> 8))
+		st.WriteByte(byte(v))
 	default:
 		panic("varint too large")
 	}
diff --git a/internal/http3/stream_test.go b/internal/http3/stream_test.go
index e08e4c5..0ea7fb3 100644
--- a/internal/http3/stream_test.go
+++ b/internal/http3/stream_test.go
@@ -193,7 +193,7 @@
 		if err := st1.Flush(); err != nil {
 			t.Fatal(err)
 		}
-		st1.stream.CloseWrite()
+		st1.CloseWrite()
 
 		if _, err := st2.readFrameHeader(); err == nil {
 			t.Fatalf("%v/%v bytes of frame available: st.readFrameHeader() succeeded; want error", i, len(frame))
@@ -206,7 +206,7 @@
 	st1.writeVarint(1)          // type
 	st1.writeVarint(100)        // size
 	st1.Write(make([]byte, 50)) // data
-	st1.stream.CloseWrite()
+	st1.CloseWrite()
 	if _, err := st2.readFrameHeader(); err != nil {
 		t.Fatalf("st.readFrameHeader() = %v", err)
 	}
@@ -219,7 +219,7 @@
 	st1, st2 := newStreamPair(t)
 	st1.writeVarint(1)   // type
 	st1.writeVarint(100) // size
-	st1.stream.CloseWrite()
+	st1.CloseWrite()
 	if _, err := st2.readFrameHeader(); err != nil {
 		t.Fatalf("st.readFrameHeader() = %v", err)
 	}
@@ -242,7 +242,7 @@
 	}
 
 	st1.Write(data)         // data
-	st1.stream.CloseWrite() // end stream
+	st1.CloseWrite() // end stream
 	got := make([]byte, len(data)+1)
 	if n, err := st2.Read(got); err != nil || n != len(data) || !bytes.Equal(got[:n], data) {
 		t.Fatalf("st.Read() = %v, %v (data=%x); want %v, nil (data=%x)", n, err, got[:n], len(data), data)
@@ -297,7 +297,7 @@
 	st1.writeVarint(typ)              // type
 	st1.writeVarint(int64(len(data))) // size
 	st1.Write(data)                   // data
-	st1.stream.CloseWrite()
+	st1.CloseWrite()
 
 	if got, err := st2.readFrameHeader(); err != nil || got != typ {
 		t.Fatalf("st.readFrameHeader() = %v, %v; want %v, nil", got, err, typ)
diff --git a/internal/http3/transport.go b/internal/http3/transport.go
index 52d9f7c..16daa24 100644
--- a/internal/http3/transport.go
+++ b/internal/http3/transport.go
@@ -6,7 +6,9 @@
 
 import (
 	"context"
+	"errors"
 	"fmt"
+	"math"
 	"net/http"
 	"net/url"
 	"sync"
@@ -24,6 +26,7 @@
 type transport struct {
 	// config is the QUIC configuration used for client connections.
 	config *quic.Config
+	tr1    *http.Transport
 
 	listenQUIC func(addr string, config *quic.Config) (*quic.Endpoint, error)
 
@@ -51,8 +54,8 @@
 	panic("netHTTPTransport.RoundTrip should never be called")
 }
 
-func (t netHTTPTransport) DialClientConn(ctx context.Context, addr string, _ *url.URL, _ func()) (http.RoundTripper, error) {
-	return t.transport.dial(ctx, addr)
+func (t netHTTPTransport) DialClientConn(ctx context.Context, addr string, _ *url.URL, stateHook func()) (http.RoundTripper, error) {
+	return t.transport.dial(ctx, addr, stateHook)
 }
 
 type TransportOpts struct {
@@ -85,6 +88,7 @@
 	tr3 := &transport{
 		// initConfig will clone the tr.TLSClientConfig.
 		config:      initConfig(opts.QUICConfig),
+		tr1:         tr,
 		listenQUIC:  opts.ListenQUIC,
 		activeConns: make(map[*clientConn]struct{}),
 	}
@@ -138,7 +142,7 @@
 }
 
 // dial creates a new HTTP/3 client connection.
-func (tr *transport) dial(ctx context.Context, target string) (*clientConn, error) {
+func (tr *transport) dial(ctx context.Context, target string, stateHook func()) (*clientConn, error) {
 	tr.incInFlightDials()
 	defer tr.decInFlightDials()
 
@@ -149,7 +153,7 @@
 	if err != nil {
 		return nil, err
 	}
-	return tr.newClientConn(ctx, qconn)
+	return tr.newClientConn(ctx, qconn, stateHook)
 }
 
 // CloseIdleConnections is called by net/http.Transport.CloseIdleConnections
@@ -172,11 +176,20 @@
 //
 // Multiple goroutines may invoke methods on a clientConn simultaneously.
 type clientConn struct {
+	tr *transport
+
 	qconn *quic.Conn
 	genericConn
 
 	enc qpackEncoder
 	dec qpackDecoder
+
+	// Guarded by genericConn.mu
+	reserved int
+	active   int
+	closed   bool
+
+	stateHook func()
 }
 
 func (tr *transport) registerConn(cc *clientConn) {
@@ -191,9 +204,11 @@
 	delete(tr.activeConns, cc)
 }
 
-func (tr *transport) newClientConn(ctx context.Context, qconn *quic.Conn) (*clientConn, error) {
+func (tr *transport) newClientConn(ctx context.Context, qconn *quic.Conn, stateHook func()) (*clientConn, error) {
 	cc := &clientConn{
-		qconn: qconn,
+		tr:        tr,
+		qconn:     qconn,
+		stateHook: stateHook,
 	}
 	tr.registerConn(cc)
 	cc.enc.init()
@@ -209,12 +224,15 @@
 
 	go func() {
 		cc.acceptStreams(qconn, cc)
+		cc.mu.Lock()
+		cc.closed = true
+		cc.mu.Unlock()
 		tr.unregisterConn(cc)
+		cc.maybeCallStateHook()
 	}()
 	return cc, nil
 }
 
-// TODO: implement the rest of net/http.ClientConn methods beyond Close.
 func (cc *clientConn) Close() error {
 	// We need to use Close rather than Abort on the QUIC connection.
 	// Otherwise, when a net/http.Transport.CloseIdleConnections is called, it
@@ -226,22 +244,63 @@
 }
 
 func (cc *clientConn) Err() error {
+	cc.mu.Lock()
+	defer cc.mu.Unlock()
+	if cc.closed {
+		return errors.New("connection closed")
+	}
 	return nil
 }
 
 func (cc *clientConn) Reserve() error {
+	cc.mu.Lock()
+	defer cc.mu.Unlock()
+	if cc.closed {
+		return errors.New("connection closed")
+	}
+	cc.reserved++
 	return nil
 }
 
 func (cc *clientConn) Release() {
+	cc.mu.Lock()
+	defer cc.mu.Unlock()
+	// This is consistent with RoundTrip: both Release and RoundTrip will
+	// consume a reservation iff one exists.
+	if cc.reserved > 0 {
+		cc.reserved--
+	}
 }
 
 func (cc *clientConn) Available() int {
-	return 0
+	cc.mu.Lock()
+	defer cc.mu.Unlock()
+	if cc.closed {
+		return 0
+	}
+	// The general recommendation for HTTP/3 is to reuse the same connection
+	// for multiple requests rather than creating new connections. As of now,
+	// we don't have a good understanding of when one might want to create
+	// multiple HTTP/3 connections to the same server.
+	// Therefore, for ClientConn API, let HTTP/3 connections have no limit.
+	// Starting a new RoundTrip when we are at the connection limit will just
+	// block until a new max stream limit is received.
+	return math.MaxInt
 }
 
 func (cc *clientConn) InFlight() int {
-	return 0
+	cc.mu.Lock()
+	defer cc.mu.Unlock()
+	if cc.closed {
+		return 0
+	}
+	return cc.reserved + cc.active
+}
+
+func (cc *clientConn) maybeCallStateHook() {
+	if cc.stateHook != nil {
+		cc.stateHook()
+	}
 }
 
 func (cc *clientConn) handleControlStream(st *stream) error {
diff --git a/internal/http3/transport_test.go b/internal/http3/transport_test.go
index 6d532f9..7d35525 100644
--- a/internal/http3/transport_test.go
+++ b/internal/http3/transport_test.go
@@ -11,6 +11,7 @@
 	"fmt"
 	"io"
 	"maps"
+	"math"
 	"net/http"
 	"reflect"
 	"slices"
@@ -34,6 +35,111 @@
 	})
 }
 
+func TestClientConnMethods(t *testing.T) {
+	synctest.Test(t, func(t *testing.T) {
+		var called bool
+		hook := func() {
+			if called {
+				t.Error("state hook was unexpectedly called")
+			}
+			called = true
+		}
+		verifyHookWasCalled := func() {
+			if !called {
+				t.Error("state hook was unexpectedly not called")
+			}
+			called = false
+		}
+		tc := newTestClientConnWithHook(t, hook)
+		tc.greet()
+
+		// Initial state after establishing connection.
+		if err := tc.cc.Err(); err != nil {
+			t.Errorf("cc.Err() = %v, want nil", err)
+		}
+		if avail := tc.cc.Available(); avail != math.MaxInt {
+			t.Errorf("cc.Available() = %v, want %v", avail, math.MaxInt)
+		}
+		if inFlight := tc.cc.InFlight(); inFlight != 0 {
+			t.Errorf("cc.InFlight() = %v, want 0", inFlight)
+		}
+
+		// Release, with Reserve before and without.
+		for _, reserveBefore := range []bool{true, false} {
+			if reserveBefore {
+				if err := tc.cc.Reserve(); err != nil {
+					t.Fatalf("cc.Reserve() failed: %v", err)
+				}
+				if avail := tc.cc.Available(); avail != math.MaxInt {
+					t.Errorf("after Reserve, cc.Available() = %v, want %v", avail, math.MaxInt)
+				}
+				if inFlight := tc.cc.InFlight(); inFlight != 1 {
+					t.Errorf("after Reserve, cc.InFlight() = %v, want 1", inFlight)
+				}
+			}
+			tc.cc.Release()
+			if avail := tc.cc.Available(); avail != math.MaxInt {
+				t.Errorf("after Release, cc.Available() = %v, want %v", avail, math.MaxInt)
+			}
+			if inFlight := tc.cc.InFlight(); inFlight != 0 {
+				t.Errorf("after Release, cc.InFlight() = %v, want 0", inFlight)
+			}
+		}
+
+		// RoundTrip, with Reserve before and without.
+		for _, reserveBefore := range []bool{true, false} {
+			if reserveBefore {
+				if err := tc.cc.Reserve(); err != nil {
+					t.Fatalf("cc.Reserve() failed: %v", err)
+				}
+				if avail := tc.cc.Available(); avail != math.MaxInt {
+					t.Errorf("after Reserve, cc.Available() = %v, want %v", avail, math.MaxInt)
+				}
+				if inFlight := tc.cc.InFlight(); inFlight != 1 {
+					t.Errorf("after Reserve, cc.InFlight() = %v, want 1", inFlight)
+				}
+			}
+			req, _ := http.NewRequest("GET", "https://example.com/", nil)
+			rt := tc.roundTrip(req)
+			if avail := tc.cc.Available(); avail != math.MaxInt {
+				t.Errorf("after RoundTrip, cc.Available() = %v, want %v", avail, math.MaxInt)
+			}
+			st := tc.wantStream(streamTypeRequest)
+			st.wantHeaders(nil)
+			st.writeHeaders(http.Header{":status": []string{"200"}})
+			resp := rt.response()
+			if resp.StatusCode != 200 {
+				t.Errorf("resp.StatusCode = %v, want 200", resp.StatusCode)
+			}
+			if inFlight := tc.cc.InFlight(); inFlight != 1 { // InFlight should decrement only after the body is closed.
+				t.Errorf("before body close, cc.InFlight() = %v, want 1", inFlight)
+			}
+			resp.Body.Close()
+			if inFlight := tc.cc.InFlight(); inFlight != 0 {
+				t.Errorf("after body close, cc.InFlight() = %v, want 0", inFlight)
+			}
+			verifyHookWasCalled()
+		}
+
+		// Connection closure.
+		if err := tc.cc.Reserve(); err != nil {
+			t.Fatalf("cc.Reserve() failed: %v", err)
+		}
+		tc.cc.Close()
+		synctest.Wait()
+		verifyHookWasCalled()
+		if err := tc.cc.Err(); err == nil {
+			t.Error("after connection is closed, cc.Err() = nil, want err")
+		}
+		if avail := tc.cc.Available(); avail != 0 {
+			t.Errorf("after connection is closed, cc.Available() = %v, want 0", avail)
+		}
+		if inFlight := tc.cc.InFlight(); inFlight != 0 {
+			t.Errorf("after connection is closed, cc.InFlight() = %v, want 0", inFlight)
+		}
+	})
+}
+
 // A testQUICConn wraps a *quic.Conn and provides methods for inspecting it.
 type testQUICConn struct {
 	t       testing.TB
@@ -353,7 +459,7 @@
 func (ts *testQUICStream) wantError(want quic.StreamErrorCode) {
 	ts.t.Helper()
 	synctest.Wait()
-	_, err := ts.stream.stream.ReadByte()
+	_, err := ts.ReadByte()
 	if err == nil {
 		ts.t.Fatalf("successfully read from stream; want stream error code %v", want)
 	}
@@ -440,17 +546,18 @@
 	control *testQUICStream
 }
 
-func newTestClientConn(t testing.TB) *testClientConn {
+func newTestClientConnWithHook(t testing.TB, stateHook func()) *testClientConn {
 	e1, e2 := newQUICEndpointPair(t)
 	tr := &transport{
 		endpoint: e1,
 		config: &quic.Config{
 			TLSConfig: testTLSConfig,
 		},
+		tr1:         new(http.Transport),
 		activeConns: make(map[*clientConn]struct{}),
 	}
 
-	cc, err := tr.dial(t.Context(), e2.LocalAddr().String())
+	cc, err := tr.dial(t.Context(), e2.LocalAddr().String(), stateHook)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -471,6 +578,10 @@
 	return tc
 }
 
+func newTestClientConn(t testing.TB) *testClientConn {
+	return newTestClientConnWithHook(t, nil)
+}
+
 // greet performs initial connection handshaking with the client.
 func (tc *testClientConn) greet() {
 	// Client creates a control stream.
diff --git a/webdav/file.go b/webdav/file.go
index 15196f9..b6f5edd 100644
--- a/webdav/file.go
+++ b/webdav/file.go
@@ -61,6 +61,10 @@
 // Dir does not prevent traversal of symbolic links within its directory tree,
 // including links that reference locations outside of the tree.
 //
+// Dir does not prevent filesystem modifications which change the target
+// of relative symbolic links. For example, moving a directory containing
+// a symbolic link to "../target" may cause the link to point to a new target.
+//
 // While the FileSystem.OpenFile method takes '/'-separated paths, a Dir's
 // string value is a filename on the native file system, not a URL, so it is
 // separated by filepath.Separator, which isn't necessarily '/'.
diff --git a/webdav/webdav.go b/webdav/webdav.go
index aecc47a..6ba4f6c 100644
--- a/webdav/webdav.go
+++ b/webdav/webdav.go
@@ -14,7 +14,8 @@
 //
 //   - Remote code execution.
 //   - Access of a file outside of the restricted tree
-//     defined by [Dir].
+//     defined by [Dir] (but see the Dir documentation for
+//     limitations).
 //
 // Other misbahaviors (crashes, excessive resource consumption,
 // lack of isolation between clients, etc.) will be handled
diff --git a/xsrftoken/xsrf.go b/xsrftoken/xsrf.go
index e808e6d..5d588f3 100644
--- a/xsrftoken/xsrf.go
+++ b/xsrftoken/xsrf.go
@@ -20,9 +20,11 @@
 // It is exported so clients may set cookie timeouts that match generated tokens.
 const Timeout = 24 * time.Hour
 
-// clean sanitizes a string for inclusion in a token by replacing all ":" with "::".
+// clean sanitizes a string for inclusion in a token by replacing _ with __ and : with _c.
 func clean(s string) string {
-	return strings.Replace(s, `:`, `::`, -1)
+	s = strings.ReplaceAll(s, `_`, `__`)
+	s = strings.ReplaceAll(s, `:`, `_c`)
+	return s
 }
 
 // Generate returns a URL-safe secure XSRF token that expires in 24 hours.
diff --git a/xsrftoken/xsrf_test.go b/xsrftoken/xsrf_test.go
index 60ff84a..14978dc 100644
--- a/xsrftoken/xsrf_test.go
+++ b/xsrftoken/xsrf_test.go
@@ -65,11 +65,16 @@
 			generateTokenAtTime("key", ":foo:", "wah", now),
 			generateTokenAtTime("key", "::foo::", "wah", now),
 		},
+		{
+			"End Colon and Start Colon",
+			generateTokenAtTime("key", "foo:", "wah", now),
+			generateTokenAtTime("key", "foo", ":wah", now),
+		},
 	}
 
 	for _, st := range separatorTests {
 		if st.token1 == st.token2 {
-			t.Errorf("%v: Expected generated tokens to be different", st.name)
+			t.Errorf("%v: Expected generated tokens (%q) to be different", st.name, st.token1)
 		}
 	}
 }