UNPKG

@ng1005/chrome-extension-common

Version:

chrome扩展通用库--消息与storage

305 lines (292 loc) 9.91 kB
/** * 获取标签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(', '); };