diff --git a/css_test.go b/css_test.go
index 4de7ea1..b0170d9 100644
--- a/css_test.go
+++ b/css_test.go
@@ -806,6 +806,26 @@ func TestSelector(t *testing.T) {
}
}
+func TestHexEscapeMatchesID(t *testing.T) {
+ s, err := Parse(`#\31 0`)
+ if err != nil {
+ t.Fatal(err)
+ }
+ root, err := html.Parse(strings.NewReader(`
x
y
z
`))
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := s.Select(root)
+ if len(got) != 1 {
+ t.Fatalf("matches=%d, want the element with id 10", len(got))
+ }
+ for _, a := range got[0].Attr {
+ if a.Key == "id" && a.Val != "10" {
+ t.Fatalf("id=%q, want 10", a.Val)
+ }
+ }
+}
+
func TestBadSelector(t *testing.T) {
tests := []struct {
sel string
diff --git a/lex.go b/lex.go
index 5977e69..2e3c275 100644
--- a/lex.go
+++ b/lex.go
@@ -4,6 +4,7 @@ import (
"fmt"
"strconv"
"strings"
+ "unicode"
"unicode/utf8"
)
@@ -359,33 +360,37 @@ func (l *lexer) consumeEscape(b *strings.Builder) error {
return nil
}
+ // The digit already consumed is part of the value. At most five more
+ // follow it (six total). Whitespace ends the escape so a later hex
+ // digit stays in the identifier, which is how #\31 0 names id "10".
var hexRune strings.Builder
- n := 0
- for {
- r := l.peek()
- if isHex(r) {
- l.pop()
- n++
- if n > 5 {
- return l.errorf("too many hex digits consuming escape sequence")
- }
- hexRune.WriteRune(r)
- continue
- }
+ hexRune.WriteRune(r)
+ for n := 1; n < 6 && isHex(l.peek()); n++ {
+ hexRune.WriteRune(l.pop())
+ }
+ if isHex(l.peek()) {
+ return l.errorf("too many hex digits consuming escape sequence")
+ }
+ if isWhitespace(l.peek()) {
+ l.pop()
+ }
- if isWhitespace(r) {
- l.pop()
- continue
- }
+ s := hexRune.String()
+ val, err := strconv.ParseUint(s, 16, 64)
+ if err != nil {
+ return l.errorf("failed to parse hex escape sequence %s: %v", s, err)
+ }
+ b.WriteRune(escapedCodePoint(val))
+ return nil
+}
- s := hexRune.String()
- val, err := strconv.ParseUint(s, 16, 64)
- if err != nil {
- return l.errorf("failed to parse hex escape sequence %s: %v", s, err)
- }
- b.WriteRune(rune(val))
- return nil
+// escapedCodePoint applies the CSS rule for a hex escape value.
+// Zero, a surrogate, or a value past the Unicode maximum is U+FFFD.
+func escapedCodePoint(val uint64) rune {
+ if val == 0 || val > unicode.MaxRune || (val >= 0xD800 && val <= 0xDFFF) {
+ return '\uFFFD'
}
+ return rune(val)
}
// https://www.w3.org/TR/css-syntax-3/#consume-a-name
diff --git a/lex_test.go b/lex_test.go
index d7007e5..34e7675 100644
--- a/lex_test.go
+++ b/lex_test.go
@@ -60,7 +60,7 @@ func TestLexer(t *testing.T) {
` "\0a f" `,
[]token{
tok(tokenWhitespace, " "),
- tok(tokenString, `"\0a f"`, "¯"),
+ tok(tokenString, `"\0a f"`, "\nf"),
tok(tokenWhitespace, " "),
},
},
@@ -84,6 +84,18 @@ func TestLexer(t *testing.T) {
tok(tokenHash, `#\0100`, "#Ā").withFlag(tokenFlagID),
},
},
+ {
+ `#\31 0`,
+ []token{
+ tok(tokenHash, `#\31 0`, "#10").withFlag(tokenFlagID),
+ },
+ },
+ {
+ `\26 B`,
+ []token{
+ tok(tokenIdent, `\26 B`, "&B"),
+ },
+ },
{
`#foo()`,
[]token{