@ng1005/chrome-extension-common
Version:
chrome扩展通用库--消息与storage
305 lines (292 loc) • 9.91 kB
text/typescript
/**
* 获取标签css selector
* @param res
* @ignoreId 是否忽略id选择器
* @returns
*/
export function getSelector(res:HTMLElement,ignoreId=false){
if(!res||!res.tagName)return '';
let tagName=res.tagName.toLowerCase();
let id=res.id
let clazz=res.getAttribute('class')||''
let selector:string[]=[]
clazz.trim().split(' ').forEach(o=>{
if(o.indexOf('active')==-1&&o.trim()!=''){
selector.push(o)
}
})
if(id&&!ignoreId){
return tagName+'#'+id
}else if(clazz){
return (tagName+'.'+selector.join('.'))//.toLowerCase();
}else{//没有任何标识的时候只能取上级
let index=prevIndex(res)
let pSelector=parentSelector(res,ignoreId)
let ppSelector=''
if(index==-1){
ppSelector=parentSelector(res.parentElement,ignoreId)
}
return (ppSelector?(ppSelector+'>'):'')+pSelector+'>'+tagName+(index==-1?'':`:nth-child(${index})`)
// return pSelector+'>'+tagName+(index==-1?'':`:nth-child(${index})`)
// return pSelector+'>'+tagName+`:eq(${index})`
}
}
/**
* 获取当前元素排行位置
* @param element
* @returns
*/
function prevIndex(element:HTMLElement) {//获取当前节点的上一个元素节点
let index=1;
let el=element.previousSibling
do {
if (el&&el.nodeType !=3){
index++;
}
if(el){
el = el.previousSibling;
}
} while (el);
let flag=false;//有同级别一样的元素时需要
let children=element.parentNode?.childNodes||[]
let pos=0;
for(let i=0;i<children.length;i++){
let c=children[i]
if(c.nodeType!=3){
pos++
}
if(c.tagName==element.tagName&&(pos!=index)){
flag=true;
}
}
return flag?index:-1;
}
/**
* 获取上级selector
* @param element
* @returns
*/
export function parentSelector(element:HTMLElement,ignoreId=false) {
let p=element
if(!p)return ''
let selector='';
do {
p = p.parentNode
if(p){
selector=getSelector(p,ignoreId)
}
} while (!selector&&p);
return selector;
}
/**
* 获取所有上级节点的selector
* @param {*} element
* @returns
*/
export function parentsSelector(element:HTMLElement,ignoreId=false) {
let p=element
let selectors:string[]=[]
do {
let selector='';
p = p.parentNode
if(p){
selector=getSelector(p,ignoreId)
if(selector){
selectors.push(selector)
}
}
} while (p);
return selectors;
}
export type Options = {
ignoreId: boolean
};
const defaultOptions: Options = {
ignoreId: false
};
/**
* 获取xpath
* @param el
* @param customOptions
* @returns
*/
export function getXPath( el: any, customOptions?: Partial< Options > ): string {
const options = { ...defaultOptions, ...customOptions };
let nodeElem = el;
if ( nodeElem && nodeElem.id && ! options.ignoreId ) {
return "//*[@id=\"" + nodeElem.id + "\"]";
}
let parts: string[] = [];
while ( nodeElem && ( 1 === nodeElem.nodeType || 3 === nodeElem.nodeType ) ) {
let numberOfPreviousSiblings = 0;
let hasNextSiblings = false;
let sibling = nodeElem.previousSibling;
while ( sibling ) {
if ( sibling.nodeType !== 10 &&
sibling.nodeName === nodeElem.nodeName
) {
numberOfPreviousSiblings++;
}
sibling = sibling.previousSibling;
}
sibling = nodeElem.nextSibling;
while ( sibling ) {
if ( sibling.nodeName === nodeElem.nodeName ) {
hasNextSiblings = true;
break;
}
sibling = sibling.nextSibling;
}
let prefix = nodeElem.prefix ? nodeElem.prefix + ":" : "";
let nth = numberOfPreviousSiblings || hasNextSiblings
? "[" + ( numberOfPreviousSiblings + 1 ) + "]"
: "";
let piece = ( nodeElem.nodeType != 3 )
? prefix + nodeElem.localName + nth
: 'text()' + ( nth || '[1]' );
parts.push( piece );
nodeElem = nodeElem.parentNode;
if ( nodeElem && nodeElem.id && ! options.ignoreId ) {
parts.push("/*[@id=\"" + nodeElem.id + "\"]")
break;
}
}
return parts.length ? "/" + parts.reverse().join( "/" ) : "";
}
const isValidXPath = (expr:string) => (
typeof expr != 'undefined' &&
expr.replace(/[\s-_=]/g,'') !== '' &&
expr.length === expr.replace(/[-_\w:.]+\(\)\s*=|=\s*[-_\w:.]+\(\)|\sor\s|\sand\s|\[(?:[^\/\]]+[\/\[]\/?.+)+\]|starts-with\(|\[.*last\(\)\s*[-\+<>=].+\]|number\(\)|not\(|count\(|text\(|first\(|normalize-space|[^\/]following-sibling|concat\(|descendant::|parent::|self::|child::|/gi,'').length
);
const getValidationRegex = () => {
let regex =
"(?P<node>"+
"("+
"^id\\([\"\\']?(?P<idvalue>%(value)s)[\"\\']?\\)"+// special case! `id(idValue)`
"|"+
"(?P<nav>//?(?:following-sibling::)?)(?P<tag>%(tag)s)" + // `//div`
"(\\[("+
"(?P<matched>(?P<mattr>@?%(attribute)s=[\"\\'](?P<mvalue>%(value)s))[\"\\']"+ // `[@id="well"]` supported and `[text()="yes"]` is not
"|"+
"(?P<contained>contains\\((?P<cattr>@?%(attribute)s,\\s*[\"\\'](?P<cvalue>%(value)s)[\"\\']\\))"+// `[contains(@id, "bleh")]` supported and `[contains(text(), "some")]` is not
")\\])?"+
"(\\[\\s*(?P<nth>\\d+|last\\(\\s*\\))\\s*\\])?"+
")"+
")";
const subRegexes = {
"tag": "([a-zA-Z][a-zA-Z0-9:-]*|\\*)",
"attribute": "[.a-zA-Z_:][-\\w:.]*(\\(\\))?)",
"value": "\\s*[\\w/:][-/\\w\\s,:;.]*"
};
Object.keys(subRegexes).forEach(key => {
regex = regex.replace(new RegExp('%\\(' + key + '\\)s', 'gi'), subRegexes[key]);
});
regex = regex.replace(/\?P<node>|\?P<idvalue>|\?P<nav>|\?P<tag>|\?P<matched>|\?P<mattr>|\?P<mvalue>|\?P<contained>|\?P<cattr>|\?P<cvalue>|\?P<nth>/gi, '');
return new RegExp(regex, 'gi');
};
function preParseXpath (expr:string){
return expr.replace(/contains\s*\(\s*concat\(["']\s+["']\s*,\s*@class\s*,\s*["']\s+["']\)\s*,\s*["']\s+([a-zA-Z0-9-_]+)\s+["']\)/gi, '@class="$1"')
}
/**
* 将xpath转成css selector
* @param expr
* @returns
*/
export function xPathToCss(expr:string) {
if (!expr) {
throw new Error('Missing XPath expression');
}
expr = preParseXpath(expr);
if (!isValidXPath(expr)) {
throw new Error('Invalid or unsupported XPath: ' + expr);
}
const xPathArr = expr.split('|');
const prog = getValidationRegex();
const cssSelectors = [];
let xindex = 0;
while (xPathArr[xindex]) {
const css = [];
let position = 0;
let nodes;
while (nodes = prog.exec(xPathArr[xindex])) {
let attr;
if (!nodes && position === 0) {
throw new Error('Invalid or unsupported XPath: ' + expr);
}
const match = {
node: nodes[5],
idvalue: nodes[12] || nodes[3],
nav: nodes[4],
tag: nodes[5],
matched: nodes[7],
mattr: nodes[10] || nodes[14],
mvalue: nodes[12] || nodes[16],
contained: nodes[13],
cattr: nodes[14],
cvalue: nodes[16],
nth: nodes[18]
};
let nav = '';
if (position != 0 && match['nav']) {
if (~match['nav'].indexOf('following-sibling::')) {
nav = ' + ';
} else {
nav = (match['nav'] == '//') ? ' ' : ' > ';
}
}
const tag = (match['tag'] === '*') ? '' : (match['tag'] || '');
if (match['contained']) {
if (match['cattr'].indexOf('@') === 0) {
attr = '[' + match['cattr'].replace(/^@/, '') + '*="' + match['cvalue'] + '"]';
} else {
throw new Error('Invalid or unsupported XPath attribute: ' + match['cattr']);
}
} else if (match['matched']) {
switch (match['mattr']) {
case '@id':
attr = '#' + match['mvalue'].replace(/^\s+|\s+$/,'').replace(/\s/g, '#');
break;
case '@class':
attr = '.' + match['mvalue'].replace(/^\s+|\s+$/,'').replace(/\s/g, '.');
break;
case 'text()':
case '.':
throw new Error('Invalid or unsupported XPath attribute: ' + match['mattr']);
default:
if (match['mattr'].indexOf('@') !== 0) {
throw new Error('Invalid or unsupported XPath attribute: ' + match['mattr']);
}
if (match['mvalue'].indexOf(' ') !== -1) {
match['mvalue'] = '\"' + match['mvalue'].replace(/^\s+|\s+$/,'') + '\"';
}
attr = '[' + match['mattr'].replace('@', '') + '="' + match['mvalue'] + '"]';
break;
}
} else if (match['idvalue']) {
attr = '#' + match['idvalue'].replace(/\s/, '#');
} else {
attr = '';
}
let nth = '';
if (match['nth']) {
if (match['nth'].indexOf('last') === -1) {
if (isNaN(parseInt(match['nth'], 10))) {
throw new Error('Invalid or unsupported XPath attribute: ' + match['nth']);
}
nth = parseInt(match['nth'], 10) !== 1 ? ':nth-of-type(' + match['nth'] + ')' : ':first-of-type';
} else {
nth = ':last-of-type';
}
}
css.push(nav + tag + attr + nth);
position++;
}
const result = css.join('');
if (result === '') {
throw new Error('Invalid or unsupported XPath');
}
cssSelectors.push(result);
xindex++;
}
return cssSelectors.join(', ');
};