Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions css_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(`<div id="10">x</div><div id="1">y</div><div id="310">z</div>`))
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
Expand Down
51 changes: 28 additions & 23 deletions lex.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"strconv"
"strings"
"unicode"
"unicode/utf8"
)

Expand Down Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion lex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, " "),
},
},
Expand All @@ -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{
Expand Down