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{