Skip to content

Commit 3279aea

Browse files
committed
internal/parser: add complete an+b support
This code is a big mess because it's hard to deconstruct ambiguities in css lexer tokens that are not taken into consideration for the an+b microsyntax. This will have to do for now. We might consider instead writing a separate parser (not on the current lexer) for an+b or just retaining the string content without parsing an+b nodes into a semantically useful node.
1 parent 42670a5 commit 3279aea

File tree

3 files changed

+115
-14
lines changed

3 files changed

+115
-14
lines changed

internal/parser/selectors.go

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package parser
22

33
import (
4+
"strings"
5+
46
"github.com/stephen/cssc/internal/ast"
57
"github.com/stephen/cssc/internal/lexer"
68
)
@@ -126,10 +128,10 @@ func (p *parser) parseSelector() *ast.Selector {
126128

127129
if pc.Name == "nth-child" || pc.Name == "nth-last-child" || pc.Name == "nth-of-type" || pc.Name == "nth-last-of-type" {
128130
switch p.lexer.Current {
129-
case lexer.Number:
131+
case lexer.Number, lexer.Dimension:
130132
pc.Arguments = p.parseANPlusB()
131133
case lexer.Ident:
132-
if p.lexer.CurrentString == "n" {
134+
if p.lexer.CurrentString == "n" || p.lexer.CurrentString == "-n" {
133135
pc.Arguments = p.parseANPlusB()
134136
break
135137
}
@@ -224,18 +226,66 @@ func (p *parser) parseANPlusB() *ast.ANPlusB {
224226

225227
v := &ast.ANPlusB{Span: p.lexer.TokenSpan()}
226228

227-
if p.lexer.Current == lexer.Number {
229+
if p.lexer.Current == lexer.Dimension && p.lexer.CurrentString == "n" {
228230
v.A = p.lexer.CurrentNumeral
231+
v.End = p.lexer.TokenEnd()
229232
p.lexer.Next()
230-
}
233+
} else if p.lexer.Current == lexer.Dimension && strings.HasPrefix(p.lexer.CurrentString, "n") {
234+
v.A = p.lexer.CurrentNumeral
235+
v.End = p.lexer.TokenEnd()
231236

232-
if p.lexer.CurrentString != "n" {
233-
p.lexer.Errorf("expected literal n as part of An+B")
237+
numeral := p.lexer.CurrentString[1:]
238+
239+
if strings.HasPrefix(numeral, "-") {
240+
v.Operator = "-"
241+
} else if strings.HasPrefix(numeral, "+") {
242+
v.Operator = "+"
243+
} else {
244+
p.lexer.Errorf("expected +/- as part of An+B")
245+
}
246+
247+
v.B = numeral[1:]
248+
if len(v.B) == 0 {
249+
p.lexer.Errorf("expected number after operator")
250+
}
251+
252+
v.End = p.lexer.TokenEnd()
253+
p.lexer.Next()
254+
} else if p.lexer.Current == lexer.Ident && p.lexer.CurrentString == "n" {
255+
v.End = p.lexer.TokenEnd()
256+
p.lexer.Expect(lexer.Ident)
257+
} else if p.lexer.Current == lexer.Ident && p.lexer.CurrentString == "-n" {
258+
v.A = "-1"
259+
v.End = p.lexer.TokenEnd()
260+
p.lexer.Expect(lexer.Ident)
261+
} else if p.lexer.Current == lexer.Number {
262+
v.A = "0"
263+
v.B = p.lexer.CurrentNumeral
264+
if strings.HasPrefix(p.lexer.CurrentNumeral, "-") {
265+
v.Operator = "-"
266+
v.B = v.B[1:]
267+
} else if strings.HasPrefix(p.lexer.CurrentNumeral, "+") {
268+
v.Operator = "+"
269+
v.B = v.B[1:]
270+
}
271+
v.End = p.lexer.TokenEnd()
272+
p.lexer.Expect(lexer.Number)
234273
}
235-
v.End = p.lexer.TokenEnd()
236-
p.lexer.Expect(lexer.Ident)
237274

238-
if p.lexer.Current == lexer.Delim && (p.lexer.CurrentString == "+" || p.lexer.CurrentString == "-") {
275+
// If there was no whitespace, e.g. n+3, then the lexer will have given
276+
// us a number. Otherwise, it'll be n + 3 with a delimiter.
277+
if p.lexer.Current == lexer.Number {
278+
if strings.HasPrefix(p.lexer.CurrentNumeral, "-") {
279+
v.Operator = "-"
280+
} else if strings.HasPrefix(p.lexer.CurrentNumeral, "+") {
281+
v.Operator = "+"
282+
} else {
283+
p.lexer.Errorf("expected +/- as part of An+B")
284+
}
285+
v.B = p.lexer.CurrentNumeral[1:]
286+
v.End = p.lexer.TokenEnd()
287+
p.lexer.Expect(lexer.Number)
288+
} else if p.lexer.Current == lexer.Delim && (p.lexer.CurrentString == "+" || p.lexer.CurrentString == "-") {
239289
v.Operator = p.lexer.CurrentString
240290
p.lexer.Next()
241291

internal/printer/printer.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -296,12 +296,25 @@ func (p *printer) print(in ast.Node) {
296296
}
297297

298298
case *ast.ANPlusB:
299-
if node.A != "" {
300-
p.s.WriteString(node.A)
299+
wrote := false
300+
if node.A != "" && node.A != "1" && node.A != "0" {
301+
if node.A == "-1" {
302+
p.s.WriteString("-")
303+
} else if strings.HasPrefix(node.A, "+") {
304+
p.s.WriteString(node.A[1:])
305+
} else {
306+
p.s.WriteString(node.A)
307+
}
308+
wrote = true
309+
}
310+
if node.A != "0" {
311+
p.s.WriteRune('n')
312+
wrote = true
301313
}
302-
p.s.WriteRune('n')
303-
if node.B != "" {
304-
p.s.WriteRune('+')
314+
if node.B != "" && node.B != "0" {
315+
if wrote {
316+
p.s.WriteString(node.Operator)
317+
}
305318
p.s.WriteString(node.B)
306319
}
307320

internal/printer/printer_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,41 @@ func TestRule_NoSemicolon(t *testing.T) {
5757
assert.Equal(t, `.class{width:2rem}`,
5858
Print(t, `.class { width: 2rem }`))
5959
}
60+
61+
func TestANPlusB(t *testing.T) {
62+
assert.Equal(t, `:nth-child(n+3){}`, Print(t, `:nth-child(n + 3) {}`))
63+
assert.Equal(t, `:nth-child(n+3){}`, Print(t, `:nth-child(n+ 3) {}`))
64+
assert.Equal(t, `:nth-child(n+3){}`, Print(t, `:nth-child(n+3) {}`))
65+
66+
assert.Equal(t, `:nth-child(2n-1){}`, Print(t, `:nth-child(2n-1) {}`))
67+
assert.Equal(t, `:nth-child(2n-1){}`, Print(t, `:nth-child(2n -1) {}`))
68+
assert.Equal(t, `:nth-child(2n-1){}`, Print(t, `:nth-child(2n - 1) {}`))
69+
70+
assert.Equal(t, `:nth-child(2n+3){}`, Print(t, `:nth-child(2n + 3) {}`))
71+
assert.Equal(t, `:nth-child(2n+3){}`, Print(t, `:nth-child(2n +3) {}`))
72+
73+
assert.Equal(t, `:nth-child(-n+6){}`, Print(t, `:nth-child(-n + 6) {}`))
74+
assert.Equal(t, `:nth-child(-2n+6){}`, Print(t, `:nth-child(-2n + 6) {}`))
75+
76+
assert.Equal(t, `:nth-child(n){}`, Print(t, `:nth-child(n) {}`))
77+
assert.Equal(t, `:nth-child(-n){}`, Print(t, `:nth-child(-n) {}`))
78+
79+
// From https://www.w3.org/TR/css-syntax-3/#anb-microsyntax.
80+
assert.Equal(t, `:nth-child(2n){}`, Print(t, `:nth-child(2n+0) {}`))
81+
assert.Equal(t, `:nth-child(even){}`, Print(t, `:nth-child(even) {}`))
82+
assert.Equal(t, `:nth-child(4n+1){}`, Print(t, `:nth-child(4n+1) {}`))
83+
assert.Equal(t, `:nth-child(-n+6){}`, Print(t, `:nth-child(-1n+6) {}`))
84+
assert.Equal(t, `:nth-child(-4n+10){}`, Print(t, `:nth-child(-4n+10) {}`))
85+
assert.Equal(t, `:nth-child(5){}`, Print(t, `:nth-child(0n+5) {}`))
86+
assert.Equal(t, `:nth-child(5){}`, Print(t, `:nth-child(5) {}`))
87+
assert.Equal(t, `:nth-child(n){}`, Print(t, `:nth-child(1n+0) {}`))
88+
assert.Equal(t, `:nth-child(n){}`, Print(t, `:nth-child(n+0) {}`))
89+
assert.Equal(t, `:nth-child(n){}`, Print(t, `:nth-child(n) {}`))
90+
assert.Equal(t, `:nth-child(2n){}`, Print(t, `:nth-child(2n+0) {}`))
91+
assert.Equal(t, `:nth-child(2n){}`, Print(t, `:nth-child(2n) {}`))
92+
assert.Equal(t, `:nth-child(3n-6){}`, Print(t, `:nth-child(3n-6) {}`))
93+
assert.Equal(t, `:nth-child(3n+1){}`, Print(t, `:nth-child(3n + 1) {}`))
94+
assert.Equal(t, `:nth-child(3n-2){}`, Print(t, `:nth-child(+3n - 2) {}`))
95+
assert.Equal(t, `:nth-child(-n+6){}`, Print(t, `:nth-child(-n+ 6) {}`))
96+
assert.Equal(t, `:nth-child(6){}`, Print(t, `:nth-child(+6) {}`))
97+
}

0 commit comments

Comments
 (0)