Skip to content

Commit

Permalink
API for getting the node at specific source location.
Browse files Browse the repository at this point in the history
  • Loading branch information
chriseppstein committed May 4, 2018
1 parent b964b3d commit 2a00f62
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 41 deletions.
24 changes: 24 additions & 0 deletions API.md
Expand Up @@ -287,6 +287,17 @@ String(cloned);
// => #search
```

### `node.isAtPosition(line, column)`

Return a `boolean` indicating whether this node includes the character at the
position of the given line and column. Returns `undefined` if the nodes lack
sufficient source metadata to determine the position.

Arguments:

* `line`: 1-index based line number relative to the start of the selector.
* `column`: 1-index based column number relative to the start of the selector.

### `node.spaces`

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

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

### `container.atPosition(line, column)`

Returns the node at the source position `index`.

```js
selector.at(0) === selector.first;
selector.at(0) === selector.nodes[0];
```

Arguments:

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

### `container.index(node)`

Return the index of the node within its container.
Expand Down
22 changes: 22 additions & 0 deletions postcss-selector-parser.d.ts
Expand Up @@ -164,6 +164,13 @@ declare namespace parser {
next(): Node;
prev(): Node;
clone(opts: {[override: string]:any}): Node;
/**
* Return whether this node includes the character at the position of the given line and column.
* Returns undefined if the nodes lack sufficient source metadata to determine the position.
* @param line 1-index based line number relative to the start of the selector.
* @param column 1-index based column number relative to the start of the selector.
*/
isAtPosition(line: number, column: number): boolean | undefined;
/**
* Some non-standard syntax doesn't follow normal escaping rules for css,
* this allows the escaped value to be specified directly, allowing illegal characters to be
Expand Down Expand Up @@ -201,6 +208,20 @@ declare namespace parser {
append(selector: Selector): Container;
prepend(selector: Selector): Container;
at(index: number): Node;
/**
* Return the most specific node at the line and column number given.
* The source location is based on the original parsed location, locations aren't
* updated as selector nodes are mutated.
*
* Note that this location is relative to the location of the first character
* of the selector, and not the location of the selector in the overall document
* when used in conjunction with postcss.
*
* If not found, returns undefined.
* @param line The line number of the node to find. (1-based index)
* @param col The column number of the node to find. (1-based index)
*/
atPosition(line: number, column: number): Node;
index(child: Node): number;
readonly first: Node;
readonly last: Node;
Expand Down Expand Up @@ -259,6 +280,7 @@ declare namespace parser {
* a postcss Rule node, a better error message is raised.
*/
error(message: string, options?: ErrorOptions): Error;
nodeAt(line: number, column: number): Node
}
function root(opts: ContainerOptions): Root;
function isRoot(node: any): node is Root;
Expand Down
18 changes: 18 additions & 0 deletions src/__tests__/comments.js
Expand Up @@ -5,6 +5,24 @@ test('comments', '/*test comment*/h2', (t, tree) => {
t.deepEqual(tree.nodes[0].nodes[1].value, 'h2');
});

test('comments', '.a /*test comment*/label', (t, tree) => {
t.deepEqual(tree.nodes[0].nodes[0].type, 'class');
t.deepEqual(tree.nodes[0].nodes[1].type, 'combinator');
t.deepEqual(tree.nodes[0].nodes[1].value, ' ');
t.deepEqual(tree.nodes[0].nodes[1].spaces.after, ' ');
t.deepEqual(tree.nodes[0].nodes[1].rawSpaceAfter, ' /*test comment*/');
t.deepEqual(tree.nodes[0].nodes[2].type, 'tag');
});

test('comments', '.a /*test comment*/ label', (t, tree) => {
t.deepEqual(tree.nodes[0].nodes[0].type, 'class');
t.deepEqual(tree.nodes[0].nodes[1].type, 'combinator');
t.deepEqual(tree.nodes[0].nodes[1].value, ' ');
t.deepEqual(tree.nodes[0].nodes[1].spaces.before, ' ');
t.deepEqual(tree.nodes[0].nodes[1].rawSpaceBefore, ' /*test comment*/ ');
t.deepEqual(tree.nodes[0].nodes[2].type, 'tag');
});

test('multiple comments and other things', 'h1/*test*/h2/*test*/.test/*test*/', (t, tree) => {
t.deepEqual(tree.nodes[0].nodes[0].type, 'tag', 'should have a tag');
t.deepEqual(tree.nodes[0].nodes[1].type, 'comment', 'should have a comment');
Expand Down
52 changes: 52 additions & 0 deletions src/__tests__/container.js
Expand Up @@ -349,3 +349,55 @@ test('container#insertAfter (during iteration)', (t) => {
});
t.deepEqual(out, 'h1[class], h2[class], h3[class]');
});

test('Container#atPosition first pseudo', (t) => {
parse(':not(.foo),\n#foo > :matches(ol, ul)', (root) => {
let node = root.atPosition(1, 1);
t.deepEqual(node.type, "pseudo");
t.deepEqual(node.toString(), ":not(.foo)");
});
});

test('Container#atPosition class in pseudo', (t) => {
parse(':not(.foo),\n#foo > :matches(ol, ul)', (root) => {
let node = root.atPosition(1, 6);
t.deepEqual(node.type, "class");
t.deepEqual(node.toString(), ".foo");
});
});

test('Container#atPosition id in second selector', (t) => {
parse(':not(.foo),\n#foo > :matches(ol, ul)', (root) => {
let node = root.atPosition(2, 1);
t.deepEqual(node.type, "id");
t.deepEqual(node.toString(), "\n#foo");
});
});

test('Container#atPosition combinator in second selector', (t) => {
parse(':not(.foo),\n#foo > :matches(ol, ul)', (root) => {
let node = root.atPosition(2, 6);
t.deepEqual(node.type, "combinator");
t.deepEqual(node.toString(), " > ");

let nodeSpace = root.atPosition(2, 5);
t.deepEqual(nodeSpace.type, "selector");
t.deepEqual(nodeSpace.toString(), "\n#foo > :matches(ol, ul)");
});
});

test('Container#atPosition tag in second selector pseudo', (t) => {
parse(':not(.foo),\n#foo > :matches(ol, ul)', (root) => {
let node = root.atPosition(2, 17);
t.deepEqual(node.type, "tag");
t.deepEqual(node.toString(), "ol");
});
});

test('Container#atPosition comma in second selector pseudo', (t) => {
parse(':not(.foo),\n#foo > :matches(ol, ul)', (root) => {
let node = root.atPosition(2, 19);
t.deepEqual(node.type, "pseudo");
t.deepEqual(node.toString(), ":matches(ol, ul)");
});
});
40 changes: 40 additions & 0 deletions src/__tests__/node.js
Expand Up @@ -80,3 +80,43 @@ test('Node#setPropertyWithoutEscape without existing raws', (t) => {
});
t.deepEqual(out, '.w+t+f');
});

test('Node#isAtPosition', (t) => {
parse(':not(.foo),\n#foo > :matches(ol, ul)', (root) => {
t.deepEqual(root.isAtPosition(1, 1), true);
t.deepEqual(root.isAtPosition(1, 10), true);
t.deepEqual(root.isAtPosition(2, 23), true);
t.deepEqual(root.isAtPosition(2, 24), false);
let selector = root.first;
t.deepEqual(selector.isAtPosition(1, 1), true);
t.deepEqual(selector.isAtPosition(1, 10), true);
t.deepEqual(selector.isAtPosition(1, 11), false);
let pseudoNot = selector.first;
t.deepEqual(pseudoNot.isAtPosition(1, 1), true);
t.deepEqual(pseudoNot.isAtPosition(1, 7), true);
t.deepEqual(pseudoNot.isAtPosition(1, 10), true);
t.deepEqual(pseudoNot.isAtPosition(1, 11), false);
let notSelector = pseudoNot.first;
t.deepEqual(notSelector.isAtPosition(1, 1), false);
t.deepEqual(notSelector.isAtPosition(1, 4), false);
t.deepEqual(notSelector.isAtPosition(1, 5), true);
t.deepEqual(notSelector.isAtPosition(1, 6), true);
t.deepEqual(notSelector.isAtPosition(1, 9), true);
t.deepEqual(notSelector.isAtPosition(1, 10), true);
t.deepEqual(notSelector.isAtPosition(1, 11), false);
let notClass = notSelector.first;
t.deepEqual(notClass.isAtPosition(1, 5), false);
t.deepEqual(notClass.isAtPosition(1, 6), true);
t.deepEqual(notClass.isAtPosition(1, 9), true);
t.deepEqual(notClass.isAtPosition(1, 10), false);
let secondSel = root.at(1);
t.deepEqual(secondSel.isAtPosition(1, 11), false);
t.deepEqual(secondSel.isAtPosition(2, 1), true);
t.deepEqual(secondSel.isAtPosition(2, 23), true);
t.deepEqual(secondSel.isAtPosition(2, 24), false);
let combinator = secondSel.at(1);
t.deepEqual(combinator.isAtPosition(2, 5), false);
t.deepEqual(combinator.isAtPosition(2, 6), true);
t.deepEqual(combinator.isAtPosition(2, 7), false);
});
});
22 changes: 11 additions & 11 deletions src/__tests__/sourceIndex.js
Expand Up @@ -6,16 +6,16 @@ test('universal selector', '*', (t, tree) => {
t.deepEqual(tree.nodes[0].nodes[0].sourceIndex, 0);
});

test('lobotomized owl selector', '* + *', (t, tree) => {
t.deepEqual(tree.nodes[0].nodes[0].source.start.column, 1);
t.deepEqual(tree.nodes[0].nodes[0].source.end.column, 1);
t.deepEqual(tree.nodes[0].nodes[0].sourceIndex, 0);
t.deepEqual(tree.nodes[0].nodes[1].source.start.column, 3);
t.deepEqual(tree.nodes[0].nodes[1].source.end.column, 3);
t.deepEqual(tree.nodes[0].nodes[1].sourceIndex, 2);
t.deepEqual(tree.nodes[0].nodes[2].source.start.column, 5);
t.deepEqual(tree.nodes[0].nodes[2].source.end.column, 5);
t.deepEqual(tree.nodes[0].nodes[2].sourceIndex, 4);
test('lobotomized owl selector', ' * + * ', (t, tree) => {
t.deepEqual(tree.nodes[0].nodes[0].source.start.column, 2);
t.deepEqual(tree.nodes[0].nodes[0].source.end.column, 2);
t.deepEqual(tree.nodes[0].nodes[0].sourceIndex, 1);
t.deepEqual(tree.nodes[0].nodes[1].source.start.column, 4);
t.deepEqual(tree.nodes[0].nodes[1].source.end.column, 4);
t.deepEqual(tree.nodes[0].nodes[1].sourceIndex, 3);
t.deepEqual(tree.nodes[0].nodes[2].source.start.column, 6);
t.deepEqual(tree.nodes[0].nodes[2].source.end.column, 6);
t.deepEqual(tree.nodes[0].nodes[2].sourceIndex, 5);
});

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

t.deepEqual(tree.nodes[0].nodes[5].source.start.line, 1, "' ' start line");
t.deepEqual(tree.nodes[0].nodes[5].source.start.column, 21, "' ' start column");
t.deepEqual(tree.nodes[0].nodes[5].source.end.column, 21, "' ' end column");
t.deepEqual(tree.nodes[0].nodes[5].source.end.column, 23, "' ' end column");
t.deepEqual(tree.nodes[0].nodes[5].sourceIndex, 20, "' ' sourceIndex");
});

Expand Down

0 comments on commit 2a00f62

Please sign in to comment.