Skip to content

Commit 3450630

Browse files
authored
Fix position calculations when offset is missing (#1983)
1 parent 77420d6 commit 3450630

File tree

2 files changed

+109
-6
lines changed

2 files changed

+109
-6
lines changed

lib/node.js

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,36 @@ function cloneNode(obj, parent) {
3232
return cloned
3333
}
3434

35+
function sourceOffset(inputCSS, position) {
36+
// Not all custom syntaxes support `offset` in `source.start` and `source.end`
37+
if (
38+
position &&
39+
typeof position.offset !== 'undefined'
40+
) {
41+
return position.offset;
42+
}
43+
44+
let column = 1
45+
let line = 1
46+
let offset = 0
47+
48+
for (let i = 0; i < inputCSS.length; i++) {
49+
if (line === position.line && column === position.column) {
50+
offset = i
51+
break
52+
}
53+
54+
if (inputCSS[i] === '\n') {
55+
column = 1
56+
line += 1
57+
} else {
58+
column += 1
59+
}
60+
}
61+
62+
return offset
63+
}
64+
3565
class Node {
3666
constructor(defaults = {}) {
3767
this.raws = {}
@@ -174,12 +204,15 @@ class Node {
174204
return this.parent.nodes[index + 1]
175205
}
176206

177-
positionBy(opts, stringRepresentation) {
207+
positionBy(opts) {
178208
let pos = this.source.start
179209
if (opts.index) {
180210
pos = this.positionInside(opts.index)
181211
} else if (opts.word) {
182-
stringRepresentation = this.source.input.css.slice(this.source.start.offset, this.source.end.offset)
212+
let stringRepresentation = this.source.input.css.slice(
213+
sourceOffset(this.source.input.css, this.source.start),
214+
sourceOffset(this.source.input.css, this.source.end)
215+
)
183216
let index = stringRepresentation.indexOf(opts.word)
184217
if (index !== -1) pos = this.positionInside(index)
185218
}
@@ -189,7 +222,7 @@ class Node {
189222
positionInside(index) {
190223
let column = this.source.start.column
191224
let line = this.source.start.line
192-
let offset = this.source.start.offset
225+
let offset = sourceOffset(this.source.input.css, this.source.start)
193226
let end = offset + index
194227

195228
for (let i = offset; i < end; i++) {
@@ -226,13 +259,15 @@ class Node {
226259
}
227260

228261
if (opts.word) {
229-
let stringRepresentation = this.source.input.css.slice(this.source.start.offset, this.source.end.offset)
262+
let stringRepresentation = this.source.input.css.slice(
263+
sourceOffset(this.source.input.css, this.source.start),
264+
sourceOffset(this.source.input.css, this.source.end)
265+
)
230266
let index = stringRepresentation.indexOf(opts.word)
231267
if (index !== -1) {
232-
start = this.positionInside(index, stringRepresentation)
268+
start = this.positionInside(index)
233269
end = this.positionInside(
234270
index + opts.word.length,
235-
stringRepresentation
236271
)
237272
}
238273
} else {

test/node.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,22 @@ test('rangeBy() returns range for word', () => {
483483
})
484484
})
485485

486+
test('rangeBy() returns range for word when offsets are missing', () => {
487+
let css = parse('a { one: X }')
488+
let a = css.first as Rule
489+
let one = a.first as Declaration
490+
491+
// @ts-expect-error
492+
if (one.source?.start) delete one.source.start.offset;
493+
// @ts-expect-error
494+
if (one.source?.end) delete one.source.end.offset;
495+
496+
equal(one.rangeBy({ word: 'one' }), {
497+
end: { column: 9, line: 1 },
498+
start: { column: 6, line: 1 }
499+
})
500+
})
501+
486502
test('rangeBy() returns range for word even after AST mutations', () => {
487503
let css = parse('a {\n\tone: 1;\n\ttwo: 2;}')
488504
let a = css.first as Rule
@@ -510,6 +526,42 @@ test('rangeBy() returns range for word even after AST mutations', () => {
510526
})
511527
})
512528

529+
test('rangeBy() returns range for word even after AST mutations when offsets are missing', () => {
530+
let css = parse('a {\n\tone: 1;\n\ttwo: 2;}')
531+
let a = css.first as Rule
532+
let one = a.first as Declaration
533+
let two = one.next() as Declaration
534+
535+
// @ts-expect-error
536+
if (a.source?.start) delete a.source.start.offset;
537+
// @ts-expect-error
538+
if (a.source?.end) delete a.source.end.offset;
539+
// @ts-expect-error
540+
if (two.source?.start) delete two.source.start.offset;
541+
// @ts-expect-error
542+
if (two.source?.end) delete two.source.end.offset;
543+
544+
equal(a.rangeBy({ word: 'two' }), {
545+
end: { column: 5, line: 3 },
546+
start: { column: 2, line: 3 }
547+
})
548+
equal(two.rangeBy({ word: 'two' }), {
549+
end: { column: 5, line: 3 },
550+
start: { column: 2, line: 3 }
551+
})
552+
553+
one.remove()
554+
555+
equal(a.rangeBy({ word: 'two' }), {
556+
end: { column: 5, line: 3 },
557+
start: { column: 2, line: 3 }
558+
})
559+
equal(two.rangeBy({ word: 'two' }), {
560+
end: { column: 5, line: 3 },
561+
start: { column: 2, line: 3 }
562+
})
563+
})
564+
513565
test('rangeBy() returns range for index and endIndex', () => {
514566
let css = parse('a { one: X }')
515567
let a = css.first as Rule
@@ -520,6 +572,22 @@ test('rangeBy() returns range for index and endIndex', () => {
520572
})
521573
})
522574

575+
test('rangeBy() returns range for index and endIndex when offsets are missing', () => {
576+
let css = parse('a { one: X }')
577+
let a = css.first as Rule
578+
let one = a.first as Declaration
579+
580+
// @ts-expect-error
581+
if (one.source?.start) delete one.source.start.offset;
582+
// @ts-expect-error
583+
if (one.source?.end) delete one.source.end.offset;
584+
585+
equal(one.rangeBy({ endIndex: 3, index: 1 }), {
586+
end: { column: 9, line: 1 },
587+
start: { column: 7, line: 1 }
588+
})
589+
})
590+
523591
test('rangeBy() returns range for index and endIndex after AST mutations', () => {
524592
let css = parse('a {\n\tone: 1;\n\ttwo: 2;}')
525593
let a = css.first as Rule

0 commit comments

Comments
 (0)