Comparing version 1.0.0 to 1.1.0
36
index.js
@@ -48,3 +48,3 @@ const parser = require('css-what'); | ||
if (previous.isNonSiblingAxis && ! token.isTagOrUniversal) { | ||
if (previous.isNonSiblingAxis && ! token.isTagOrUniversal && ! token.isPseudoComment) { | ||
xpath.push('*'); | ||
@@ -96,6 +96,14 @@ } | ||
break; | ||
default: | ||
default: { | ||
let {data} = token; | ||
if (token.isPseudoNot) { | ||
filters.push(`not(${subExpression(token.data, {operator: 'or'})})`); | ||
filters.push(`not(${subExpression(data, {operator: 'or'})})`); | ||
} | ||
else if (token.isPseudoComment) { | ||
if (! previous.isAxis) { | ||
commitFilters(); | ||
xpath.push('/'); | ||
} | ||
xpath.push('comment()' + (data ? `[${data}]` : '')); | ||
} | ||
else { | ||
@@ -105,2 +113,3 @@ filters.push(...resolveAsFilters(token)); | ||
break; | ||
} | ||
} | ||
@@ -220,5 +229,9 @@ } | ||
case 'text-contains': // fallthrough | ||
case 'text-contains-case': { | ||
case 'text-contains-case': // fallthrough | ||
case 'text-start': // fallthrough | ||
case 'text-start-case': // fallthrough | ||
case 'text-end': // fallthrough | ||
case 'text-end-case': { | ||
// Normalizing whitespace for all text pseudos. | ||
let elementText = 'normalize-space()'; | ||
let text = 'normalize-space()'; | ||
let searchText = data.trim(); | ||
@@ -229,3 +242,3 @@ | ||
let abc = 'abcdefghijklmnopqrstuvwxyz'; | ||
elementText = `translate(normalize-space(), '${abc.toUpperCase()}', '${abc}')`; | ||
text = `translate(normalize-space(), '${abc.toUpperCase()}', '${abc}')`; | ||
searchText = searchText.toLowerCase(); | ||
@@ -242,6 +255,12 @@ } | ||
let textExpr = `${elementText} = ${searchText}`; | ||
let textExpr = `${text} = ${searchText}`; | ||
if (/contains/.test(name)) { | ||
textExpr = `contains(${elementText}, ${searchText})`; | ||
textExpr = `contains(${text}, ${searchText})`; | ||
} | ||
else if (/^text-start/.test(name)) { | ||
textExpr = `starts-with(${text}, ${searchText})`; | ||
} | ||
else if (/^text-end/.test(name)) { | ||
textExpr = `substring(${text}, string-length(${text})-${searchText.length-3}) = ${searchText}`; | ||
} | ||
@@ -280,2 +299,3 @@ elements.push(textExpr); | ||
token.isNonSiblingAxis = /^(descendant|child)$/.test(type); | ||
token.isAxis = token.isSiblingAxis || token.isNonSiblingAxis; | ||
token.isTagOrUniversal = /^(tag|universal)$/.test(type); | ||
@@ -282,0 +302,0 @@ return token; |
{ | ||
"name": "csstoxpath", | ||
"version": "1.0.0", | ||
"version": "1.1.0", | ||
"description": "CSS to XPath", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
[![Build Status](https://travis-ci.org/peteboere/csstoxpath.svg?branch=master)](https://travis-ci.org/peteboere/csstoxpath) | ||
## CSS to XPath | ||
# CSS to XPath | ||
Converts most CSS 2.1/3 selectors (see exclusions below) to equivalent XPath. | ||
Converts most CSS 2.1/3 selectors (see exclusions below) to equivalent XPath 1.0 expression. | ||
@@ -10,27 +10,40 @@ See `test.js` for examples. | ||
### Custom pseudo classes | ||
## Custom pseudos | ||
All `:text*` text matching pseudo classes normalize whitespace and ignore tags. | ||
To take advantage of the different capabilities of XPath several custom pseudo selectors have been implemented: | ||
#### `:text` | ||
All text matching pseudo classes normalize whitespace and ignore tags. | ||
E.g. `" my <i>string</i> "` is treated as `"my string"`. | ||
* `:text("foo")` Case-insensitive matching of element text content | ||
* `:text-case("foo")` As `:text` but case-sensitive | ||
* `:text-contains("foo")` Case-insensitive substring matching of element text content | ||
* `:text-contains-case("foo")` As `:text-contains` but case-sensitive | ||
* `:text("foo")` Case-insensitive matching of element text | ||
* `:text-case("foo")` Case-sensitive `:text-case` | ||
* `:text-contains("foo")` Case-insensitive substring matching of element text | ||
* `:text-contains-case("foo")` Case-sensitive `:text-contains` | ||
* `:text-start("foo")` Case-insensitive matching of element starting text | ||
* `:text-start-case("foo")` Case-sensitive `:text-start` | ||
* `:text-end("foo")` Case-insensitive matching of element ending text | ||
* `:text-end-case("foo")` Case-sensitive `:text-end` | ||
#### `:comment` | ||
* `:comment` Select comment nodes | ||
* `:comment(n)` Select comment nodes at child position `n` | ||
Note: Can be combined with `:text` to match based on comment text content. E.g. `p > :comment:text("foo")` | ||
#### `:childless` | ||
* `:childless` As `:empty` but ignoring whitespace | ||
### Unsupported selectors | ||
## Unsupported psuedos | ||
These may be supported at a later date: | ||
* `nth-last-child` | ||
* `:lang()` | ||
* `:nth-last-child` | ||
As far as I'm aware the following cannot be implemented in XPath: | ||
Part-dynamic pseudos are excluded as they can only be partially supported by attribute matching: | ||
* `*-of-type` | ||
The following are excluded as they can only be partially supported (XPath only matches static attributes): | ||
* `:checked` | ||
@@ -40,1 +53,8 @@ * `:disabled` | ||
* `:required` | ||
* `:lang` | ||
The following cannot be implemented in XPath 1.0: | ||
* `:*-of-type` | ||
* States: `:hover`, `:focus`, `:active`, `:visited`, `:target` etc. | ||
* Elements: `::before`, `::after`, `::first-letter` etc. |
39
test.js
@@ -1,2 +0,2 @@ | ||
/*global describe it*/ | ||
/* global describe it */ | ||
@@ -8,4 +8,4 @@ const cssToXpath = require('./index'); | ||
'Simple', | ||
['a', '//a'], | ||
['*', '//*'], | ||
['a', `//a`], | ||
['*', `//*`], | ||
['#a', `//*[@id = 'a']`], | ||
@@ -128,6 +128,35 @@ ['.a', | ||
':text-start', | ||
[':text-start("foo bar")', | ||
`//*[starts-with(translate(normalize-space(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), "foo bar")]`], | ||
':text-start-case (case sensitive)', | ||
[':text-start-case("foo bar")', | ||
`//*[starts-with(normalize-space(), "foo bar")]`], | ||
':text-end', | ||
[':text-end("foo bar")', | ||
`//*[substring(translate(normalize-space(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), string-length(translate(normalize-space(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'))-6) = "foo bar"]`], | ||
':text-end-case (case sensitive)', | ||
[':text-end-case("foo bar")', | ||
`//*[substring(normalize-space(), string-length(normalize-space())-6) = "foo bar"]`], | ||
'Unions', | ||
['a, b', `(//a|//b)`], | ||
[':first-child, .foo', | ||
`(//*[position() = 1]|//*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')])`] | ||
`(//*[position() = 1]|//*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')])`], | ||
'Comments', | ||
[':comment', `//comment()`], | ||
['a :comment', `//a//comment()`], | ||
['a > :comment', `//a/comment()`], | ||
['a:comment', `//a/comment()`], | ||
['a:comment(1)', `//a/comment()[1]`], | ||
['a:comment(1):text("Foo")', | ||
`//a/comment()[1][translate(normalize-space(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') = "foo"]`], | ||
['a:comment(1):text-case("Foo")', `//a/comment()[1][normalize-space() = "Foo"]`], | ||
['a:comment:text-contains("Foo")', | ||
`//a/comment()[contains(translate(normalize-space(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), "foo")]`], | ||
['a:comment:text-contains-case("Foo")', `//a/comment()[contains(normalize-space(), "Foo")]`] | ||
]; | ||
@@ -187,3 +216,3 @@ | ||
let unsupported = [ | ||
':nth-last-child(2)', | ||
':nth-last-child(1)', | ||
':nth-of-type', | ||
@@ -190,0 +219,0 @@ ':nth-last-of-type', |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
24011
547
59