Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

csstoxpath

Package Overview
Dependencies
Maintainers
1
Versions
9
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

csstoxpath - npm Package Compare versions

Comparing version 1.4.0 to 1.5.0

.gitattributes

2

.eslintrc.js

@@ -57,4 +57,4 @@ /*eslint sort-keys: 2*/

'no-new-object': 2,
'no-tabs': 2,
'no-template-curly-in-string': 2,
'no-tabs': 2,
'no-throw-literal': 2,

@@ -61,0 +61,0 @@ 'no-trailing-spaces': 2,

@@ -1,10 +0,31 @@

const parser = require('css-what');
'use strict';
const CSSwhat = require('css-what');
const self = module.exports = {};
self.tokenStream = (css, pseudos) => parser(self.applyCustomPsuedos(css, pseudos));
self.tokenStreams = (css, pseudos) => {
const streams = CSSwhat
.parse(self.applyCustomPsuedos(css, pseudos));
const pseudoAliases = {
first: 'first-child',
last: 'last-child',
child: 'nth-child',
contains: 'text-contains',
};
for (const stream of streams) {
for (const item of stream) {
if (item.type === 'pseudo' && pseudoAliases[item.name]) {
item.name = pseudoAliases[item.name];
}
}
}
return streams;
};
self.decorateToken = token => {
const {type, name} = token;
const titleCase = s => s.replace(/^\w/, m => m.toUpperCase());
token[`is${titleCase(type)}`] = true;

@@ -18,2 +39,3 @@ if (name) {

token.isTagOrUniversal = /^(tag|universal)$/.test(type);
return token;

@@ -23,34 +45,108 @@ };

self.translateCaseMap = str => {
const letters = str
const az = str
.toLowerCase()
// ISO/IEC 8859-15 (+ greek) lowercase letters.
.replace(/[^a-z\u0161\u017E\u0153\u00E0-\u00FF\u03AC-\u03CE]/g, '')
.split('');
const az = self.arrayUnique(letters).join('');
return {az, AZ: az.toUpperCase()};
.split('')
.filter(self.arrayFilterUnique)
.join('');
return {
az,
AZ: az.toUpperCase(),
};
};
self.arrayUnique = arr => arr.filter((v, i, a) => a.indexOf(v) === i);
self.arrayFilterUnique = (v, i, a) => a.indexOf(v) === i;
self.applyCustomPsuedos = (selector, pseudos) => {
const names = pseudos ? Object.keys(pseudos) : [];
if (! names.length) {
const keys = pseudos
? Object.keys(pseudos)
: [];
if (! keys.length) {
return selector;
}
const {string, restore} = literalCapture(selector);
const patt = new RegExp(`:(${names.join('|')})(?:\\(([^)]+)\\)|(?![\\w-]))`, 'g');
return restore(string.replace(patt, (_, name, data) => {
const handler = pseudos[name];
data = (typeof data === 'string') ? restore(data.trim()) : undefined;
return (typeof handler === 'function') ? handler(data) : handler;
}));
let {string, restore} = literalCapture(selector);
const names = keys
.filter(it => ! it.startsWith('/'));
const dataSubPatt = String.raw`(?:\((?<data>[^)]+)\)|(?![\w-]))`;
if (names.length) {
const patt = new RegExp(`:(${names.join('|')})${dataSubPatt}`, 'g');
string = string
.replace(patt, (_, name, data) => {
const handler = pseudos[name];
data = typeof data === 'string'
? restore(data.trim())
: undefined;
return typeof handler === 'function'
? handler(data)
: handler;
});
}
const regexes = keys
.filter(it => it.startsWith('/'))
.map(it => {
const source = it.slice(1, it.lastIndexOf('/'));
if (! /^[a-z]/.test(source)) {
throw new SyntaxError('Custom pesudo regexes must begin with [a-z] and not use anchors (^$)');
}
const handler = pseudos[it];
if (typeof handler !== 'function') {
throw new SyntaxError('Custom pesudo regexes must have a function handler');
}
return {
pattern: new RegExp(`:(${source})${dataSubPatt}`, 'g'),
handler,
};
});
for (const regex of regexes) {
string = string
.replace(regex.pattern, (...matches) => {
// Reduce matches to one object containing named and positional matches.
const result = {};
// Seperate data.
const named = matches.pop();
const {data} = named;
delete named.data;
Object.assign(result, named);
// Trim irrelevant match data.
matches.shift(); // Fullmatch.
matches.pop(); // Full input.
matches.pop(); // Offset.
matches.pop(); // Data.
Object.assign(result, matches);
return regex.handler(data, result);
});
}
return restore(string);
};
function literalCapture(string) {
const literalCapture = string => {
const literals = [];
return {
literals,
string: string.replace(/(["'])(?:\\\1|.)*?\1/g, m => `__S${literals.push(m)-1}__`),
restore: str => str.replace(/__S(\d+)__/g, (...m) => literals[m[1]]),
string: string
.replace(/(["'])(?:\\\1|.)*?\1/g, m => `__S${literals.push(m)-1}__`),
restore: str => str
.replace(/__S(\d+)__/g, (...m) => literals[m[1]]),
};
}
};
const titleCase = string => string.replace(/^\w/, m => m.toUpperCase());

@@ -1,7 +0,10 @@

const {tokenStream, decorateToken, translateCaseMap,
arrayUnique, applyCustomPsuedos} = require('./helpers');
'use strict';
const {tokenStreams, decorateToken, translateCaseMap,
arrayFilterUnique, applyCustomPsuedos} = require('./helpers');
const self = module.exports = function cssToXPath(css, {pseudos}={}) {
const streams = tokenStream(css, pseudos);
const streams = tokenStreams(css, pseudos);
const expressions = [];
for (const stream of streams) {

@@ -17,9 +20,29 @@ stream.forEach(token => decorateToken(token));

decorateToken(stream[0]);
let tagContext;
for (const item of stream) {
if (item.isTag) {
tagContext = item.name;
}
else if (item.isAxis) {
tagContext = null;
}
else if (tagContext) {
item.tagContext = tagContext;
}
if (item.isPseudo && item.name.endsWith('-of-type') && ! item.tagContext) {
throw new SyntaxError('*-of-type pseudos require a tag context');
}
}
expressions.push(xpathExpression(stream));
}
return (expressions.length > 1) ? `(${expressions.join('|')})` : expressions[0];
return expressions.length > 1
? `(${expressions.join('|')})`
: expressions[0];
};
self.subExpression = (css, {pseudos}={}) => {
const streams = tokenStream(css, pseudos)
const streams = tokenStreams(css, pseudos)
.map(stream => {

@@ -30,2 +53,3 @@ return stream

});
return subExpression(streams, {operator: 'or'});

@@ -37,4 +61,6 @@ };

function xpathExpression(tokens) {
let xpath = [];
let filters = [];
const commitFilters = () => {

@@ -49,5 +75,9 @@ const flattened = flattenFilters(filters);

for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
const previous = (i > 0) ? tokens[i-1] : {};
const previous = i > 0
? tokens[i-1]
: {};
if (previous.isNonSiblingAxis && ! token.isTagOrUniversal && ! token.isPseudoComment) {

@@ -115,2 +145,15 @@ xpath.push('*');

}
else if (
token.isPseudoNthChild
|| token.isPseudoFirstChild
|| token.isPseudoLastChild
|| token.isPseudoOnlyChild
) {
const tokenFilters = resolveAsFilters(token);
if (token.tagContext) {
xpath.splice(-1, 1, '*');
tokenFilters.unshift(`name() = '${token.tagContext}'`);
}
filters.push(...tokenFilters);
}
else {

@@ -125,2 +168,3 @@ filters.push(...resolveAsFilters(token));

commitFilters();
return xpath.join('');

@@ -138,2 +182,3 @@ }

});
return flattenFilters(stack, options);

@@ -193,6 +238,8 @@ }

break;
case 'first-child':
case 'first-child': // fallthrough
case 'first-of-type':
elements.push(`position() = 1`);
break;
case 'last-child':
case 'last-child': // fallthrough
case 'last-of-type':
elements.push(`position() = last()`);

@@ -203,3 +250,4 @@ break;

break;
case 'nth-child': {
case 'nth-child': // fallthrough
case 'nth-of-type': {
const aliases = {

@@ -288,2 +336,3 @@ odd: '2n+1',

}
return elements;

@@ -293,9 +342,12 @@ }

function flattenFilters(filters, {operator='and'}={}) {
filters = arrayUnique(filters.map(filter => {
if ((filters.length > 1) && /[=<>]|\b(and|or)\b/i.test(filter)) {
return `(${filter})`;
}
return filter;
}));
return filters.length ? filters.join(` ${operator} `) : '';
return filters
.map(filter => {
if ((filters.length > 1) && /[=<>]|\b(and|or)\b/i.test(filter)) {
return `(${filter})`;
}
return filter;
})
.filter(arrayFilterUnique)
.join(` ${operator} `);
}
{
"name": "csstoxpath",
"version": "1.4.0",
"version": "1.5.0",
"description": "CSS to XPath",

@@ -24,9 +24,9 @@ "main": "index.js",

"dependencies": {
"css-what": "~2.1.0"
"css-what": "~3.3.0"
},
"devDependencies": {
"chai": "~4.1.2",
"eslint": "~5.3.0",
"mocha": "~4.0.1"
"chai": "~4.2.0",
"eslint": "~7.9.0",
"mocha": "~8.1.3"
}
}

@@ -46,2 +46,10 @@ [![Build Status](https://travis-ci.org/peteboere/csstoxpath.svg?branch=master)](https://travis-ci.org/peteboere/csstoxpath)

## Aliased pseudos
* `:first` is aliased to `:first-child`
* `:last` is aliased to `:last-child`
* `:child` is aliased to `:nth-child`
* `:contains` is aliased to `:text-contains`
## Author pseudos

@@ -54,13 +62,15 @@

* If the `pseudos` option is set the CSS expression is
* preprocessed before generating the XPath expression.
* In this case as follows:
* preprocessed before generating the XPath expression:
*
* before :radio:nth(2)
* after input[type="radio"]:nth-child(2)
* :radio
* => input[type="radio"]
*
* :element-1(Hello)
* => element:child(1):contains("Hello")
*/
const cssToXpath = require('csstoxpath');
const xpathExpr = cssToXpath(':radio:nth(2)', {
const xpathExpr = cssToXpath(':radio, :element-1(Hello)', {
pseudos: {
radio: 'input[type="radio"]',
nth: data => `:nth-child(${data})`,
[/element-(\d+)/]: (data, m) => `element:child(${m[1]}):contains("${data}")`,
}

@@ -70,9 +80,16 @@ });

## Unsupported psuedos
## Limitations
The following may be supported at a later date:
The following pseudos are partially supported as they require a tag context:
* `:nth-of-type`
* `:first-of-type`
* `:last-of-type`
The following pseudos are currently unsupported:
* `:nth-last-child`
* `:nth-last-of-type`
Part-dynamic pseudos are excluded as they can only be partially supported by attribute matching:
Dynamic pseudos are excluded as they can only be partially supported by attribute matching:

@@ -87,4 +104,3 @@ * `:checked`

* `:*-of-type`
* States: `:hover`, `:focus`, `:active`, `:visited`, `:target` etc.
* Elements: `::before`, `::after`, `::first-letter` etc.

@@ -0,1 +1,2 @@

'use strict';
/*eslint no-console: 0*/

@@ -7,2 +8,3 @@

const samples = [
'Simple',

@@ -12,13 +14,7 @@ ['a', `//a`],

['#a', `//*[@id = 'a']`],
['.a',
`//*[@class and contains(concat(' ', normalize-space(@class), ' '), ' a ')]`,
],
['.a.b',
`//*[@class and contains(concat(' ', normalize-space(@class), ' '), ' a ') and contains(concat(' ', normalize-space(@class), ' '), ' b ')]`,
],
['.a', `//*[@class and contains(concat(' ', normalize-space(@class), ' '), ' a ')]`],
['.a.b', `//*[@class and contains(concat(' ', normalize-space(@class), ' '), ' a ') and contains(concat(' ', normalize-space(@class), ' '), ' b ')]`],
'Child axis',
['#a > .b:last-child',
`//*[@id = 'a']/*[@class and contains(concat(' ', normalize-space(@class), ' '), ' b ') and (position() = last())]`,
],
['#a > .b:last-child', `//*[@id = 'a']/*[@class and contains(concat(' ', normalize-space(@class), ' '), ' b ') and (position() = last())]`],

@@ -62,2 +58,3 @@ 'Decendant axis',

':first-child',
[':first', `//*[position() = 1]`],
[':first-child', `//*[position() = 1]`],

@@ -68,3 +65,7 @@ ['.a:first-child',

// first-child with tag context.
['div:first-child', `//*[(name() = 'div') and (position() = 1)]`],
':last-child',
[':last', `//*[position() = last()]`],
[':last-child', `//*[position() = last()]`],

@@ -74,4 +75,7 @@ ['.a:last-child',

],
// With tag context.
['div[foo]:last-child(4)', `//*[@foo and (name() = 'div') and (position() = last())]`],
':nth-child',
[':child(1n)', `//*[position() >= 0]`],
[':nth-child(1n)', `//*[position() >= 0]`],

@@ -90,3 +94,14 @@ [':nth-child(1n+2)', `//*[position() - 2 >= 0]`],

[':nth-child(-2n+3)', `//*[position() - 3 <= 0 and (position() - 3) mod -2 = 0]`],
// With tag context.
['div[foo]:nth-child(4)', `//*[@foo and (name() = 'div') and (position() = 4)]`],
':nth-of-type',
['div:nth-of-type(4)', `//div[position() = 4]`],
':last-of-type',
['div:last-of-type', `//div[position() = last()]`],
':first-of-type',
['div:first-of-type', `//div[position() = 1]`],
':root',

@@ -102,2 +117,4 @@ [':root', `/*`],

[':only-child', `//*[last() = 1]`],
// With tag context.
['div:only-child', `//*[(name() = 'div') and (last() = 1)]`],

@@ -131,2 +148,4 @@ ':empty',

':text-contains',
[':contains("foo bar ")',
`//*[contains(translate(normalize-space(), 'FOBAR', 'fobar'), "foo bar")]`],
[':text-contains("foo bar ")',

@@ -233,5 +252,3 @@ `//*[contains(translate(normalize-space(), 'FOBAR', 'fobar'), "foo bar")]`],

':nth-last-child(1)',
':nth-of-type',
':nth-last-of-type',
':last-of-type',
':only-of-type',

@@ -251,8 +268,23 @@ ':checked',

describe('Unspecified tag context *-of-type psuedos', function () {
const unsupported = [
':nth-of-type',
':first-of-type',
':last-of-type',
];
for (const css of unsupported) {
it(`should error for unsupported selector '${css}'`, function () {
expect(() => cssToXpath(css)).to.throw(Error, /-of-type pseudos require a tag context/);
});
}
});
describe('Author pseudo preprocessing', function () {
const pseudos = {
foo: 'foo',
// First alias can be overriden.
first: ':first-child:not(:last-child)',
nth: data => `:nth-child(${data})`,
radio: `input[type="radio"]`,
// Child alias can be overriden.
child(data) {

@@ -268,2 +300,27 @@ if (data) {

},
foo: 'foo',
nth: data => `:nth-child(${data})`,
radio: `input[type="radio"]`,
// Regexes.
[/custom-(?<position>\d+)(-(?<type>heading|link))?/](data, matches) {
let selector = `element:nth-of-type(${matches.position})`;
switch (matches.type) {
case 'heading':
selector += ' > h1';
break;
case 'link':
selector += ' > a';
break;
}
if (data) {
selector += `:contains(${data})`;
}
return selector;
},
};

@@ -284,3 +341,3 @@

]],
// Should be ignored.
// Should passthrough unaffected.
['a :first-child c', [

@@ -304,3 +361,3 @@ `a :first-child c`,

`input[type="radio"]:nth-child(2)`,
`//input[(@type = 'radio') and (position() = 2)]`,
`//*[(@type = 'radio') and (name() = 'input') and (position() = 2)]`,
]],

@@ -311,2 +368,10 @@ [':foo("(:foo)")', [

]],
[':custom-1-heading', [
'element:nth-of-type(1) > h1',
`//element[position() = 1]/h1`,
]],
[':custom-2-link(Some text)', [
'element:nth-of-type(2) > a:contains(Some text)',
`//element[position() = 2]/a[contains(translate(normalize-space(), 'SOMETX', 'sometx'), "some text")]`,
]],
];

@@ -313,0 +378,0 @@

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc