Comparing version
346
index.js
@@ -1,345 +0,1 @@ | ||
'use strict'; | ||
const {tokenStreams, decorateToken, translateCaseMap, | ||
arrayFilterUnique, applyCustomPsuedos, quotedString} = require('./helpers'); | ||
const self = module.exports = function cssToXPath(css, {pseudos}={}) { | ||
const streams = tokenStreams(css, pseudos); | ||
const expressions = []; | ||
for (const stream of streams) { | ||
stream.forEach(token => decorateToken(token)); | ||
if (stream[0].isPseudoRoot) { | ||
stream.shift(); | ||
stream.unshift({type: 'root'}); | ||
} | ||
else { | ||
stream.unshift({type: 'descendant'}); | ||
} | ||
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]; | ||
}; | ||
self.subExpression = (css, {pseudos}={}) => { | ||
const streams = tokenStreams(css, pseudos) | ||
.map(stream => { | ||
return stream | ||
.map(token => decorateToken(token)) | ||
.filter(token => token.isPseudo || token.isAttribute || token.isTag); | ||
}); | ||
return subExpression(streams, {operator: 'or'}); | ||
}; | ||
Object.assign(self, { | ||
applyCustomPsuedos, | ||
quotedString, | ||
}); | ||
function xpathExpression(tokens) { | ||
let xpath = []; | ||
let filters = []; | ||
const commitFilters = () => { | ||
const flattened = flattenFilters(filters); | ||
if (flattened) { | ||
xpath.push(`[${flattened}]`); | ||
} | ||
filters = []; | ||
}; | ||
for (let i = 0; i < tokens.length; i++) { | ||
const token = tokens[i]; | ||
const previous = i > 0 | ||
? tokens[i-1] | ||
: {}; | ||
if (previous.isNonSiblingAxis && ! token.isTagOrUniversal && ! token.isPseudoComment) { | ||
xpath.push('*'); | ||
} | ||
// Sibling axes. | ||
if (token.isSiblingAxis) { | ||
commitFilters(); | ||
continue; | ||
} | ||
if (previous.isAdjacent) { | ||
xpath.push('/following-sibling::*'); | ||
if (token.isTag) { | ||
filters.push(...resolveAsFilters(token)); | ||
} | ||
filters.push(`position() = 1`); | ||
if (token.isTagOrUniversal) { | ||
continue; | ||
} | ||
} | ||
else if (previous.isSibling) { | ||
const nodeName = token.isTag ? token.name : '*'; | ||
xpath.push(`/following-sibling::${nodeName}`); | ||
if (token.isTagOrUniversal) { | ||
continue; | ||
} | ||
} | ||
switch (token.type) { | ||
case 'root': | ||
xpath = ['/*']; | ||
break; | ||
case 'descendant': | ||
commitFilters(); | ||
xpath.push('//'); | ||
break; | ||
case 'child': | ||
commitFilters(); | ||
xpath.push('/'); | ||
break; | ||
case 'universal': | ||
commitFilters(); | ||
xpath.push('*'); | ||
break; | ||
case 'tag': | ||
commitFilters(); | ||
xpath.push(token.name); | ||
break; | ||
default: { | ||
const {data} = token; | ||
if (token.isPseudoNot) { | ||
filters.push(`not(${subExpression(data, {operator: 'or'})})`); | ||
} | ||
else if (token.isPseudoAny) { | ||
filters.push(self.subExpression(data)); | ||
} | ||
else if (token.isPseudoComment) { | ||
if (! previous.isAxis) { | ||
commitFilters(); | ||
xpath.push('/'); | ||
} | ||
xpath.push('comment()' + (data ? `[${data}]` : '')); | ||
} | ||
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 { | ||
filters.push(...resolveAsFilters(token)); | ||
} | ||
break; | ||
} | ||
} | ||
} | ||
commitFilters(); | ||
return xpath.join(''); | ||
} | ||
function subExpression(tokens, options={}) { | ||
const stack = []; | ||
tokens.forEach(stream => { | ||
const filters = []; | ||
stream.forEach(token => { | ||
filters.push(...resolveAsFilters(decorateToken(token))); | ||
}); | ||
stack.push(flattenFilters(filters)); | ||
}); | ||
return flattenFilters(stack, options); | ||
} | ||
function resolveAsFilters(token) { | ||
const elements = []; | ||
let {name, value, action, data} = token; | ||
if (token.isTag) { | ||
const {az, AZ} = translateCaseMap(name); | ||
elements.push(`translate(name(), '${az}', '${AZ}') = '${name.toUpperCase()}'`); | ||
} | ||
else if (token.isAttribute) { | ||
switch (action) { | ||
case 'exists': | ||
elements.push(`@${name}`); | ||
break; | ||
case 'element': | ||
elements.push(`@${name}`); | ||
elements.push(`contains(concat(' ', normalize-space(@${name}), ' '), ' ${value} ')`); | ||
break; | ||
case 'any': | ||
elements.push(`@${name}`); | ||
elements.push(`contains(@${name}, '${value}')`); | ||
break; | ||
case 'start': | ||
elements.push(`@${name}`); | ||
elements.push(`starts-with(@${name}, '${value}')`); | ||
break; | ||
case 'end': | ||
elements.push(`@${name}`); | ||
elements.push(`substring(@${name}, string-length(@${name})-${value.length-1}) = '${value}'`); | ||
break; | ||
case 'hyphen': | ||
elements.push(`@${name}`); | ||
elements.push(`(@${name} = '${value}' or starts-with(@${name}, '${value}-')`); | ||
break; | ||
case 'not': | ||
elements.push(`not(@${name}) or @${name} != '${value}'`); | ||
break; | ||
case 'equals': | ||
elements.push(`@${name} = '${value}'`); | ||
break; | ||
default: | ||
throw new SyntaxError(`Unsupported attribute selector @${name} (action: ${action}).`); | ||
} | ||
} | ||
else if (token.isPseudo) { | ||
switch (name) { | ||
case 'empty': | ||
elements.push(`not(*) and not(string-length())`); | ||
break; | ||
case 'childless': | ||
elements.push(`not(*) and not(string-length(normalize-space()))`); | ||
break; | ||
case 'first-child': // fallthrough | ||
case 'first-of-type': | ||
elements.push(`position() = 1`); | ||
break; | ||
case 'last-child': // fallthrough | ||
case 'last-of-type': | ||
elements.push(`position() = last()`); | ||
break; | ||
case 'not': | ||
elements.push(`not(${subExpression(data, {operator: 'or'})})`); | ||
break; | ||
case 'nth-child': // fallthrough | ||
case 'nth-of-type': { | ||
const aliases = { | ||
odd: '2n+1', | ||
even: '2n', | ||
}; | ||
if (aliases[data]) { | ||
data = aliases[data]; | ||
} | ||
if (/^\d+$/.test(data)) { | ||
elements.push(`position() = ${data}`); | ||
break; | ||
} | ||
const nthExpr = /^([-+])?(\d+)?n(?:\+(\d+))?$/.exec(data); | ||
if (nthExpr) { | ||
let [, sign, nth=0, offset=0] = nthExpr; | ||
nth = parseInt(nth, 10); | ||
const reverseNth = sign === '-'; | ||
if (reverseNth) { | ||
nth *= -1; | ||
} | ||
const expr = []; | ||
const position = 'position()' + (offset ? ` - ${offset}` : ''); | ||
expr.push(position); | ||
expr.push(reverseNth ? '<=' : '>='); | ||
expr.push(0); | ||
if (nth < 0 || nth > 1) { | ||
expr.push(`and (${position}) mod ${nth} = 0`); | ||
} | ||
elements.push(expr.join(' ')); | ||
break; | ||
} | ||
throw new SyntaxError(`Unrecognized nth-expression '${data}'.`); | ||
} | ||
case 'only-child': | ||
elements.push(`last() = 1`); | ||
break; | ||
case 'text': // fallthrough | ||
case 'text-case': // fallthrough | ||
case 'text-contains': // fallthrough | ||
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 text = 'normalize-space()'; | ||
let searchText = data.trim(); | ||
// Case insensitive matching. | ||
if (! /case/.test(name)) { | ||
const {az, AZ} = translateCaseMap(searchText); | ||
text = az ? `translate(normalize-space(), '${AZ}', '${az}')` : `normalize-space()`; | ||
searchText = searchText.toLowerCase(); | ||
} | ||
// Respecting authored quote method. | ||
const quoteMatch = /^(['"])([\s\S]*)\1$/.exec(searchText); | ||
const quote = quoteMatch ? quoteMatch[1] : '"'; | ||
if (quoteMatch) { | ||
searchText = quoteMatch[2].trim(); | ||
} | ||
searchText = `${quote}${searchText.replace(/\s+/g, ' ')}${quote}`; | ||
let textExpr = `${text} = ${searchText}`; | ||
if (/contains/.test(name)) { | ||
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}`; | ||
} | ||
elements.push(textExpr); | ||
break; | ||
} | ||
default: | ||
throw new SyntaxError(`Unsupported pseudo selector '${token.name}'.`); | ||
} | ||
} | ||
else { | ||
throw new SyntaxError(`Unsupported token type '${token.type}'.`); | ||
} | ||
return elements; | ||
} | ||
function flattenFilters(filters, {operator='and'}={}) { | ||
return filters | ||
.map(filter => { | ||
if ((filters.length > 1) && /[=<>]|\b(and|or)\b/i.test(filter)) { | ||
return `(${filter})`; | ||
} | ||
return filter; | ||
}) | ||
.filter(arrayFilterUnique) | ||
.join(` ${operator} `); | ||
} | ||
export {cssToXPath as default, cssToXPath} from './src/css-to-xpath.js'; |
{ | ||
"name": "csstoxpath", | ||
"version": "1.6.0", | ||
"version": "2.0.0", | ||
"description": "CSS to XPath", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "mocha test", | ||
"lint": "eslint --fix ." | ||
"lint": "eslint --fix .", | ||
"test": "node --test ./test/test.js", | ||
"ts-check": "tsc -p jsconfig.json" | ||
}, | ||
@@ -21,12 +22,13 @@ "repository": { | ||
"engines": { | ||
"node": ">=8" | ||
"node": ">=18" | ||
}, | ||
"dependencies": { | ||
"css-what": "~3.3.0" | ||
"css-what": "~6.1.0" | ||
}, | ||
"devDependencies": { | ||
"chai": "~4.2.0", | ||
"eslint": "~7.9.0", | ||
"mocha": "~8.1.3" | ||
} | ||
"@types/node": "~20.6.2", | ||
"eslint": "~8.49.0", | ||
"typescript": "~5.2.2" | ||
}, | ||
"type": "module" | ||
} |
@@ -1,3 +0,1 @@ | ||
[](https://travis-ci.org/peteboere/csstoxpath) | ||
# CSS to XPath | ||
@@ -4,0 +2,0 @@ |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
40577
2.45%10
25%947
2.49%Yes
NaN101
-1.94%1
Infinity%+ Added
- Removed
Updated