Skip to content

Commit 2a00f62

Browse files
committed
API for getting the node at specific source location.
1 parent b964b3d commit 2a00f62

File tree

10 files changed

+294
-41
lines changed

10 files changed

+294
-41
lines changed

API.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,17 @@ String(cloned);
287287
// => #search
288288
```
289289

290+
### `node.isAtPosition(line, column)`
291+
292+
Return a `boolean` indicating whether this node includes the character at the
293+
position of the given line and column. Returns `undefined` if the nodes lack
294+
sufficient source metadata to determine the position.
295+
296+
Arguments:
297+
298+
* `line`: 1-index based line number relative to the start of the selector.
299+
* `column`: 1-index based column number relative to the start of the selector.
300+
290301
### `node.spaces`
291302

292303
Extra whitespaces around the node will be moved into `node.spaces.before` and
@@ -381,6 +392,19 @@ Arguments:
381392

382393
* `index`: The index of the node to return.
383394

395+
### `container.atPosition(line, column)`
396+
397+
Returns the node at the source position `index`.
398+
399+
```js
400+
selector.at(0) === selector.first;
401+
selector.at(0) === selector.nodes[0];
402+
```
403+
404+
Arguments:
405+
406+
* `index`: The index of the node to return.
407+
384408
### `container.index(node)`
385409

386410
Return the index of the node within its container.

postcss-selector-parser.d.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,13 @@ declare namespace parser {
164164
next(): Node;
165165
prev(): Node;
166166
clone(opts: {[override: string]:any}): Node;
167+
/**
168+
* Return whether this node includes the character at the position of the given line and column.
169+
* Returns undefined if the nodes lack sufficient source metadata to determine the position.
170+
* @param line 1-index based line number relative to the start of the selector.
171+
* @param column 1-index based column number relative to the start of the selector.
172+
*/
173+
isAtPosition(line: number, column: number): boolean | undefined;
167174
/**
168175
* Some non-standard syntax doesn't follow normal escaping rules for css,
169176
* this allows the escaped value to be specified directly, allowing illegal characters to be
@@ -201,6 +208,20 @@ declare namespace parser {
201208
append(selector: Selector): Container;
202209
prepend(selector: Selector): Container;
203210
at(index: number): Node;
211+
/**
212+
* Return the most specific node at the line and column number given.
213+
* The source location is based on the original parsed location, locations aren't
214+
* updated as selector nodes are mutated.
215+
*
216+
* Note that this location is relative to the location of the first character
217+
* of the selector, and not the location of the selector in the overall document
218+
* when used in conjunction with postcss.
219+
*
220+
* If not found, returns undefined.
221+
* @param line The line number of the node to find. (1-based index)
222+
* @param col The column number of the node to find. (1-based index)
223+
*/
224+
atPosition(line: number, column: number): Node;
204225
index(child: Node): number;
205226
readonly first: Node;
206227
readonly last: Node;
@@ -259,6 +280,7 @@ declare namespace parser {
259280
* a postcss Rule node, a better error message is raised.
260281
*/
261282
error(message: string, options?: ErrorOptions): Error;
283+
nodeAt(line: number, column: number): Node
262284
}
263285
function root(opts: ContainerOptions): Root;
264286
function isRoot(node: any): node is Root;

src/__tests__/comments.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,24 @@ test('comments', '/*test comment*/h2', (t, tree) => {
55
t.deepEqual(tree.nodes[0].nodes[1].value, 'h2');
66
});
77

8+
test('comments', '.a /*test comment*/label', (t, tree) => {
9+
t.deepEqual(tree.nodes[0].nodes[0].type, 'class');
10+
t.deepEqual(tree.nodes[0].nodes[1].type, 'combinator');
11+
t.deepEqual(tree.nodes[0].nodes[1].value, ' ');
12+
t.deepEqual(tree.nodes[0].nodes[1].spaces.after, ' ');
13+
t.deepEqual(tree.nodes[0].nodes[1].rawSpaceAfter, ' /*test comment*/');
14+
t.deepEqual(tree.nodes[0].nodes[2].type, 'tag');
15+
});
16+
17+
test('comments', '.a /*test comment*/ label', (t, tree) => {
18+
t.deepEqual(tree.nodes[0].nodes[0].type, 'class');
19+
t.deepEqual(tree.nodes[0].nodes[1].type, 'combinator');
20+
t.deepEqual(tree.nodes[0].nodes[1].value, ' ');
21+
t.deepEqual(tree.nodes[0].nodes[1].spaces.before, ' ');
22+
t.deepEqual(tree.nodes[0].nodes[1].rawSpaceBefore, ' /*test comment*/ ');
23+
t.deepEqual(tree.nodes[0].nodes[2].type, 'tag');
24+
});
25+
826
test('multiple comments and other things', 'h1/*test*/h2/*test*/.test/*test*/', (t, tree) => {
927
t.deepEqual(tree.nodes[0].nodes[0].type, 'tag', 'should have a tag');
1028
t.deepEqual(tree.nodes[0].nodes[1].type, 'comment', 'should have a comment');

src/__tests__/container.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,3 +349,55 @@ test('container#insertAfter (during iteration)', (t) => {
349349
});
350350
t.deepEqual(out, 'h1[class], h2[class], h3[class]');
351351
});
352+
353+
test('Container#atPosition first pseudo', (t) => {
354+
parse(':not(.foo),\n#foo > :matches(ol, ul)', (root) => {
355+
let node = root.atPosition(1, 1);
356+
t.deepEqual(node.type, "pseudo");
357+
t.deepEqual(node.toString(), ":not(.foo)");
358+
});
359+
});
360+
361+
test('Container#atPosition class in pseudo', (t) => {
362+
parse(':not(.foo),\n#foo > :matches(ol, ul)', (root) => {
363+
let node = root.atPosition(1, 6);
364+
t.deepEqual(node.type, "class");
365+
t.deepEqual(node.toString(), ".foo");
366+
});
367+
});
368+
369+
test('Container#atPosition id in second selector', (t) => {
370+
parse(':not(.foo),\n#foo > :matches(ol, ul)', (root) => {
371+
let node = root.atPosition(2, 1);
372+
t.deepEqual(node.type, "id");
373+
t.deepEqual(node.toString(), "\n#foo");
374+
});
375+
});
376+
377+
test('Container#atPosition combinator in second selector', (t) => {
378+
parse(':not(.foo),\n#foo > :matches(ol, ul)', (root) => {
379+
let node = root.atPosition(2, 6);
380+
t.deepEqual(node.type, "combinator");
381+
t.deepEqual(node.toString(), " > ");
382+
383+
let nodeSpace = root.atPosition(2, 5);
384+
t.deepEqual(nodeSpace.type, "selector");
385+
t.deepEqual(nodeSpace.toString(), "\n#foo > :matches(ol, ul)");
386+
});
387+
});
388+
389+
test('Container#atPosition tag in second selector pseudo', (t) => {
390+
parse(':not(.foo),\n#foo > :matches(ol, ul)', (root) => {
391+
let node = root.atPosition(2, 17);
392+
t.deepEqual(node.type, "tag");
393+
t.deepEqual(node.toString(), "ol");
394+
});
395+
});
396+
397+
test('Container#atPosition comma in second selector pseudo', (t) => {
398+
parse(':not(.foo),\n#foo > :matches(ol, ul)', (root) => {
399+
let node = root.atPosition(2, 19);
400+
t.deepEqual(node.type, "pseudo");
401+
t.deepEqual(node.toString(), ":matches(ol, ul)");
402+
});
403+
});

src/__tests__/node.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,43 @@ test('Node#setPropertyWithoutEscape without existing raws', (t) => {
8080
});
8181
t.deepEqual(out, '.w+t+f');
8282
});
83+
84+
test('Node#isAtPosition', (t) => {
85+
parse(':not(.foo),\n#foo > :matches(ol, ul)', (root) => {
86+
t.deepEqual(root.isAtPosition(1, 1), true);
87+
t.deepEqual(root.isAtPosition(1, 10), true);
88+
t.deepEqual(root.isAtPosition(2, 23), true);
89+
t.deepEqual(root.isAtPosition(2, 24), false);
90+
let selector = root.first;
91+
t.deepEqual(selector.isAtPosition(1, 1), true);
92+
t.deepEqual(selector.isAtPosition(1, 10), true);
93+
t.deepEqual(selector.isAtPosition(1, 11), false);
94+
let pseudoNot = selector.first;
95+
t.deepEqual(pseudoNot.isAtPosition(1, 1), true);
96+
t.deepEqual(pseudoNot.isAtPosition(1, 7), true);
97+
t.deepEqual(pseudoNot.isAtPosition(1, 10), true);
98+
t.deepEqual(pseudoNot.isAtPosition(1, 11), false);
99+
let notSelector = pseudoNot.first;
100+
t.deepEqual(notSelector.isAtPosition(1, 1), false);
101+
t.deepEqual(notSelector.isAtPosition(1, 4), false);
102+
t.deepEqual(notSelector.isAtPosition(1, 5), true);
103+
t.deepEqual(notSelector.isAtPosition(1, 6), true);
104+
t.deepEqual(notSelector.isAtPosition(1, 9), true);
105+
t.deepEqual(notSelector.isAtPosition(1, 10), true);
106+
t.deepEqual(notSelector.isAtPosition(1, 11), false);
107+
let notClass = notSelector.first;
108+
t.deepEqual(notClass.isAtPosition(1, 5), false);
109+
t.deepEqual(notClass.isAtPosition(1, 6), true);
110+
t.deepEqual(notClass.isAtPosition(1, 9), true);
111+
t.deepEqual(notClass.isAtPosition(1, 10), false);
112+
let secondSel = root.at(1);
113+
t.deepEqual(secondSel.isAtPosition(1, 11), false);
114+
t.deepEqual(secondSel.isAtPosition(2, 1), true);
115+
t.deepEqual(secondSel.isAtPosition(2, 23), true);
116+
t.deepEqual(secondSel.isAtPosition(2, 24), false);
117+
let combinator = secondSel.at(1);
118+
t.deepEqual(combinator.isAtPosition(2, 5), false);
119+
t.deepEqual(combinator.isAtPosition(2, 6), true);
120+
t.deepEqual(combinator.isAtPosition(2, 7), false);
121+
});
122+
});

src/__tests__/sourceIndex.js

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ test('universal selector', '*', (t, tree) => {
66
t.deepEqual(tree.nodes[0].nodes[0].sourceIndex, 0);
77
});
88

9-
test('lobotomized owl selector', '* + *', (t, tree) => {
10-
t.deepEqual(tree.nodes[0].nodes[0].source.start.column, 1);
11-
t.deepEqual(tree.nodes[0].nodes[0].source.end.column, 1);
12-
t.deepEqual(tree.nodes[0].nodes[0].sourceIndex, 0);
13-
t.deepEqual(tree.nodes[0].nodes[1].source.start.column, 3);
14-
t.deepEqual(tree.nodes[0].nodes[1].source.end.column, 3);
15-
t.deepEqual(tree.nodes[0].nodes[1].sourceIndex, 2);
16-
t.deepEqual(tree.nodes[0].nodes[2].source.start.column, 5);
17-
t.deepEqual(tree.nodes[0].nodes[2].source.end.column, 5);
18-
t.deepEqual(tree.nodes[0].nodes[2].sourceIndex, 4);
9+
test('lobotomized owl selector', ' * + * ', (t, tree) => {
10+
t.deepEqual(tree.nodes[0].nodes[0].source.start.column, 2);
11+
t.deepEqual(tree.nodes[0].nodes[0].source.end.column, 2);
12+
t.deepEqual(tree.nodes[0].nodes[0].sourceIndex, 1);
13+
t.deepEqual(tree.nodes[0].nodes[1].source.start.column, 4);
14+
t.deepEqual(tree.nodes[0].nodes[1].source.end.column, 4);
15+
t.deepEqual(tree.nodes[0].nodes[1].sourceIndex, 3);
16+
t.deepEqual(tree.nodes[0].nodes[2].source.start.column, 6);
17+
t.deepEqual(tree.nodes[0].nodes[2].source.end.column, 6);
18+
t.deepEqual(tree.nodes[0].nodes[2].sourceIndex, 5);
1919
});
2020

2121
test('comment', '/**\n * Hello!\n */', (t, tree) => {
@@ -184,7 +184,7 @@ test('combinators surrounded by superfluous spaces', 'div > h1 ~ span a',
184184

185185
t.deepEqual(tree.nodes[0].nodes[5].source.start.line, 1, "' ' start line");
186186
t.deepEqual(tree.nodes[0].nodes[5].source.start.column, 21, "' ' start column");
187-
t.deepEqual(tree.nodes[0].nodes[5].source.end.column, 21, "' ' end column");
187+
t.deepEqual(tree.nodes[0].nodes[5].source.end.column, 23, "' ' end column");
188188
t.deepEqual(tree.nodes[0].nodes[5].sourceIndex, 20, "' ' sourceIndex");
189189
});
190190

0 commit comments

Comments
 (0)