Skip to content

Commit 9a7077b

Browse files
committed
fix: node end offset
1 parent ce9f6b3 commit 9a7077b

2 files changed

Lines changed: 306 additions & 0 deletions

File tree

lib/parser.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class Parser {
6565
if (brackets.length === 0) {
6666
if (type === ';') {
6767
node.source.end = this.getPosition(token[2])
68+
node.source.end.offset++
6869
this.semicolon = true
6970
break
7071
} else if (type === '{') {
@@ -79,6 +80,7 @@ class Parser {
7980
}
8081
if (prev) {
8182
node.source.end = this.getPosition(prev[3] || prev[2])
83+
node.source.end.offset++
8284
}
8385
}
8486
this.end(token)
@@ -103,6 +105,7 @@ class Parser {
103105
if (last) {
104106
token = params[params.length - 1]
105107
node.source.end = this.getPosition(token[3] || token[2])
108+
node.source.end.offset++
106109
this.spaces = node.raws.between
107110
node.raws.between = ''
108111
}
@@ -171,6 +174,7 @@ class Parser {
171174
let node = new Comment()
172175
this.init(node, token[2])
173176
node.source.end = this.getPosition(token[3] || token[2])
177+
node.source.end.offset++
174178

175179
let text = token[1].slice(2, -2)
176180
if (/^\s*$/.test(text)) {
@@ -202,6 +206,7 @@ class Parser {
202206
node.source.end = this.getPosition(
203207
last[3] || last[2] || findLastWithPosition(tokens)
204208
)
209+
node.source.end.offset++
205210

206211
while (tokens[0][0] !== 'word') {
207212
if (tokens.length === 1) this.unknownWord(tokens)
@@ -320,6 +325,7 @@ class Parser {
320325

321326
if (this.current.parent) {
322327
this.current.source.end = this.getPosition(token[2])
328+
this.current.source.end.offset++
323329
this.current = this.current.parent
324330
} else {
325331
this.unexpectedClose(token)

test/location.test.ts

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
import { test } from 'uvu'
2+
import { equal } from 'uvu/assert'
3+
4+
import {
5+
AtRule,
6+
Comment,
7+
Declaration,
8+
Node,
9+
parse,
10+
Rule
11+
} from '../lib/postcss.js'
12+
13+
function checkOffset(source: string, node: Node, expected: string): void {
14+
let start = node.source!.start!.offset
15+
let end = node.source!.end!.offset
16+
equal(source.slice(start, end), expected)
17+
}
18+
19+
test('rule', () => {
20+
let source = '.a{}'
21+
let css = parse(source)
22+
23+
let rule = css.first as Rule
24+
checkOffset(source, rule, '.a{}')
25+
equal(rule.source!.start, {
26+
column: 1,
27+
line: 1,
28+
offset: 0
29+
})
30+
equal(rule.source!.end, {
31+
column: 4,
32+
line: 1,
33+
offset: 4
34+
})
35+
})
36+
37+
test('single decl (no semicolon)', () => {
38+
let source = '.a{b:c}'
39+
let css = parse(source)
40+
41+
let rule = css.first as Rule
42+
let decl = rule.first as Declaration
43+
checkOffset(source, rule, '.a{b:c}')
44+
checkOffset(source, decl, 'b:c')
45+
equal(rule.source!.start, {
46+
column: 1,
47+
line: 1,
48+
offset: 0
49+
})
50+
equal(rule.source!.end, {
51+
column: 7,
52+
line: 1,
53+
offset: 7
54+
})
55+
equal(decl.source!.start, {
56+
column: 4,
57+
line: 1,
58+
offset: 3
59+
})
60+
equal(decl.source!.end, {
61+
column: 6,
62+
line: 1,
63+
offset: 6
64+
})
65+
})
66+
67+
test('single decl (with semicolon)', () => {
68+
let source = '.a{b:c;}'
69+
let css = parse(source)
70+
71+
let rule = css.first as Rule
72+
let decl = rule.first as Declaration
73+
checkOffset(source, rule, '.a{b:c;}')
74+
checkOffset(source, decl, 'b:c;')
75+
equal(rule.source!.start, {
76+
column: 1,
77+
line: 1,
78+
offset: 0
79+
})
80+
equal(rule.source!.end, {
81+
column: 8,
82+
line: 1,
83+
offset: 8
84+
})
85+
equal(decl.source!.start, {
86+
column: 4,
87+
line: 1,
88+
offset: 3
89+
})
90+
equal(decl.source!.end, {
91+
column: 7,
92+
line: 1,
93+
offset: 7
94+
})
95+
})
96+
97+
test('two decls', () => {
98+
let source = '.a{b:c;d:e}'
99+
let css = parse(source)
100+
101+
let rule = css.first as Rule
102+
let decl1 = rule.first as Declaration
103+
let decl2 = decl1.next() as Declaration
104+
checkOffset(source, decl1, 'b:c;')
105+
checkOffset(source, decl2, 'd:e')
106+
equal(rule.source!.start, {
107+
column: 1,
108+
line: 1,
109+
offset: 0
110+
})
111+
equal(rule.source!.end, {
112+
column: 11,
113+
line: 1,
114+
offset: 11
115+
})
116+
equal(decl1.source!.start, {
117+
column: 4,
118+
line: 1,
119+
offset: 3
120+
})
121+
equal(decl1.source!.end, {
122+
column: 7,
123+
line: 1,
124+
offset: 7
125+
})
126+
equal(decl2.source!.start, {
127+
column: 8,
128+
line: 1,
129+
offset: 7
130+
})
131+
equal(decl2.source!.end, {
132+
column: 10,
133+
line: 1,
134+
offset: 10
135+
})
136+
})
137+
138+
test('...rule nested in rule', () => {
139+
let source = '.a{.b{}}'
140+
let css = parse(source)
141+
142+
let rule = css.first as Rule
143+
let rule2 = rule.first as Rule
144+
checkOffset(source, rule, '.a{.b{}}')
145+
checkOffset(source, rule2, '.b{}')
146+
equal(rule.source!.start, {
147+
column: 1,
148+
line: 1,
149+
offset: 0
150+
})
151+
equal(rule.source!.end, {
152+
column: 8,
153+
line: 1,
154+
offset: 8
155+
})
156+
equal(rule2.source!.start, {
157+
column: 4,
158+
line: 1,
159+
offset: 3
160+
})
161+
equal(rule2.source!.end, {
162+
column: 7,
163+
line: 1,
164+
offset: 7
165+
})
166+
})
167+
168+
test('at-rule with semicolon', () => {
169+
let source = '@a b;'
170+
let css = parse(source)
171+
172+
let atrule = css.first as AtRule
173+
checkOffset(source, atrule, '@a b;')
174+
equal(atrule.source!.start, {
175+
column: 1,
176+
line: 1,
177+
offset: 0
178+
})
179+
equal(atrule.source!.end, {
180+
column: 5,
181+
line: 1,
182+
offset: 5
183+
})
184+
})
185+
186+
test('unclosed at-rule', () => {
187+
let source = '@a b'
188+
let css = parse(source)
189+
190+
let atrule = css.first as AtRule
191+
checkOffset(source, atrule, '@a b')
192+
equal(atrule.source!.start, {
193+
column: 1,
194+
line: 1,
195+
offset: 0
196+
})
197+
equal(atrule.source!.end, {
198+
column: 4,
199+
line: 1,
200+
offset: 4
201+
})
202+
})
203+
204+
test('unclosed at-rule in at-rule', () => {
205+
let source = '@a{@b c}'
206+
let css = parse(source)
207+
208+
let atrule = css.first as AtRule
209+
let atrule2 = atrule.first as AtRule
210+
checkOffset(source, atrule, '@a{@b c}')
211+
checkOffset(source, atrule2, '@b c')
212+
equal(atrule.source!.start, {
213+
column: 1,
214+
line: 1,
215+
offset: 0
216+
})
217+
equal(atrule.source!.end, {
218+
column: 8,
219+
line: 1,
220+
offset: 8
221+
})
222+
equal(atrule2.source!.start, {
223+
column: 4,
224+
line: 1,
225+
offset: 3
226+
})
227+
equal(atrule2.source!.end, {
228+
column: 7,
229+
line: 1,
230+
offset: 7
231+
})
232+
})
233+
234+
test('at-rule with body', () => {
235+
let source = '@a{}'
236+
let css = parse(source)
237+
238+
let atrule = css.first as AtRule
239+
checkOffset(source, atrule, '@a{}')
240+
equal(atrule.source!.start, {
241+
column: 1,
242+
line: 1,
243+
offset: 0
244+
})
245+
equal(atrule.source!.end, {
246+
column: 4,
247+
line: 1,
248+
offset: 4
249+
})
250+
})
251+
252+
test('at-rule nested in atrule', () => {
253+
let source = '@a{@b{}}'
254+
let css = parse(source)
255+
256+
let atrule = css.first as Rule
257+
let atrule2 = atrule.first as Rule
258+
checkOffset(source, atrule, '@a{@b{}}')
259+
checkOffset(source, atrule2, '@b{}')
260+
equal(atrule.source!.start, {
261+
column: 1,
262+
line: 1,
263+
offset: 0
264+
})
265+
equal(atrule.source!.end, {
266+
column: 8,
267+
line: 1,
268+
offset: 8
269+
})
270+
equal(atrule2.source!.start, {
271+
column: 4,
272+
line: 1,
273+
offset: 3
274+
})
275+
equal(atrule2.source!.end, {
276+
column: 7,
277+
line: 1,
278+
offset: 7
279+
})
280+
})
281+
282+
test('comment', () => {
283+
let source = '/*a*/'
284+
let css = parse(source)
285+
286+
let rule = css.first as Comment
287+
checkOffset(source, rule, '/*a*/')
288+
equal(rule.source!.start, {
289+
column: 1,
290+
line: 1,
291+
offset: 0
292+
})
293+
equal(rule.source!.end, {
294+
column: 5,
295+
line: 1,
296+
offset: 5
297+
})
298+
})
299+
300+
test.run()

0 commit comments

Comments
 (0)