class CssPriority { static _TOKEN_ID = /(#[\w-]+)/g static _TOKEN_CLASS = /(\.[\w-]+)/g static _TOKEN_ATTR = /(\[[^[\]]+])/g static _TOKEN_PSEUDO_CLASS = /(:[\w-]+)/g static _TOKEN_PSEUDO_ELEM = /(::[\w-]+)/g static _TOKEN_ELEM = /([\w-]+)/g static _PSEUDO_ELEMS = [ 'first-letter', 'last-letter', 'first-line', 'last-line', 'first-child', 'last-child', 'before', 'after' ] static _SYMBOL_B = '\x02' static _SYMBOL_C = '\x03' static _SYMBOL_D = '\x04' static compare(a, b) { //在sort应用中,返回<0,则a在b之前,>0则a在b之后 for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) { //return a[i] - b[i] return b[i] - a[i]; } } return 0 } /** * parse then priority of selector (CSS Rule) * @param {String} selector css selector * @param {Object} opts * - {Boolean} opts.important is current rule important * - {Number} opts.line line number of css rule * @return {Array} array that contains 6 priority number */ static parse(selector, opts) { opts = opts || {} // priority: [ important, style, R1, R2, R3, line ] let important = opts.important ? 1 : 0 let line = opts.line || 0 let priority = [important, 0, 0, 0, 0, line] selector = selector.replace(/(::?)([\w-]+)/g, (total, left, elem) => { if (CssPriority._PSEUDO_ELEMS.indexOf(elem) >= 0) { if (left === ':') { return '::' + elem } else { return total } } else { return total } }) // replace with symbols selector = selector .replace(CssPriority._TOKEN_ATTR, CssPriority._SYMBOL_C) .replace(CssPriority._TOKEN_PSEUDO_ELEM, CssPriority._SYMBOL_D) .replace(CssPriority._TOKEN_PSEUDO_CLASS, CssPriority._SYMBOL_C) .replace(CssPriority._TOKEN_ID, CssPriority._SYMBOL_B) .replace(CssPriority._TOKEN_CLASS, CssPriority._SYMBOL_C) .replace(CssPriority._TOKEN_ELEM, CssPriority._SYMBOL_D) // count selector = selector.replace(/[\2\3\4]/g, (symbol) => { let idx = symbol.charCodeAt(0) priority[idx]++ return '<' + idx + '>' }) return priority } } class Lock { constructor() { this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } async wait() { try { let result = await this.promise; return result; } catch (error) { throw error; } } } class CssKind { static inline = 1; static match = 2; static parent = 3; } class CssRefers { _document; _cssTree; _cssAst = []; _inheritStyles = new Map([["color", null], ["font", null], ["font-family", null], ["font-size", null], ["font-style", null], ["font-weight", null], ["font-height", null], ["font-variant", null], ["font-stretch", null], ["font-size-adjust", null], ["letter-spacing", null], ["word-spacing", null], ["text-align", null], ["text-indent", null], ["text-decoration", null], ["white-space", null], ["text-shadow", null], ["text-transform", null], ["direction", null], ["caption-side", null], ["empty-cells", null], ["border-collapse", null], ["unicode-bidi", null], ["visibility", null], ["cursor", null], ["user-select", null], ["list-style", null], ["list-style-type", null], ["list-style-position", null], ["list-style-image", null], ["display", null], ["columns", null], ["column-count", null], ["column-gap", null], ["column-rule", null], ["column-span", null], ["column-width", null], ["break-before", null], ["break-after", null], ["break-inside", null], ["line-height", null], ["cursor", null], ["quotes", null] ]); _cssCount = 0; constructor(doc, cssTree) { this._document = doc; this._cssTree = cssTree; } async #initCssAst() { if (this._cssTree) { if (this._cssAst.length > 0) return; for (let i = 0; i < this._document.styleSheets.length; i++) { let styleSheet = this._document.styleSheets[i]; if (styleSheet.href) { let cssCode = await this.#getCssData(styleSheet.href); if (cssCode) { let ast = this.#cssCodeToAST(cssCode); this._cssAst.push({ file: styleSheet.href, ast: ast, code:cssCode, }); } } } } } async #getCssData(file) { let response = await fetch(file); return await response.text(); } #cssCodeToAST(cssCode) { let curAst = this._cssTree.parse(cssCode, { parseAtrulePrelude: false, parseRulePrelude: false, parseValue: false, positions: true, }); let selectors = []; //https://github.com/csstree/csstree/blob/master/docs/parsing.md#parsesource-options csstree.walk(curAst, (node) => { if (node.type === 'Rule')//node.type === 'Declaration' { let curSelector; if (node.prelude.children) { curSelector = node.prelude.children.map(child => child.raw); } else { curSelector = node.prelude.value; } if (curSelector) selectors.push({ name: curSelector.replaceAll("\r\n", " ") ,//curSelector, loc: node.prelude.loc, }); } }); return selectors; } //获取元素内联样式定义 #getInlineStyle(ele) { let ret = []; for (let i = 0; i < ele.style.length; i++) { let sn = ele.style[i]; let isImportant = false; if ((ele.style.cssText.split(sn)[1] + ";").split(";")[0].indexOf("!important") > 0) { isImportant = true; } ret.push({ name: sn, cssText: ele.style[sn], isImportant: isImportant, }); } if (ret.length <= 0) { return ret; } else { this._cssCount = _cssCount + 1; return [{ selectorName: "", declarations: "", href: "", style: ret, id: `c_${this._cssCount}`, fileIndex: null, posIndex:null, }]; } } //计算选择器的权重级别 #getSelectorPriority(selector,opts) { //github.com/yibn2008/css-priority return CssPriority.parse(selector,opts); } //获取当前元素的匹配样式 #getMatchStyle(ele) { if (!ele) return []; const matchedCSSRules = []; // 获取所有样式表 for (let i = 0; i < this._document.styleSheets.length; i++) { const styleSheet = this._document.styleSheets[i]; try { // 遍历每个样式表的规则 for (let j = 0; j < styleSheet.cssRules.length; j++) { const rule = styleSheet.cssRules[j]; let isMatch = false; if (rule.selectorText == ":root") { if (ele.tagName == "HTML") isMatch = true; } else if (rule.selectorText == ":link" || rule.selectorText == ":visited") { if (ele.tagName == "A") isMatch = true; } else if (rule.selectorText == ":disabled" || rule.selectorText == ":enabled") { if (["INPUT", "TEXTAREA","SELECT","BUTTON","FIELDSET"].indexOf(ele.tagName) >= 0) isMatch = true; } else { if (ele.matches(rule.selectorText)) { isMatch = true; } } if (isMatch) { let styles = []; if (rule.style) { let sn = ""; for (let i = 0; i < rule.style.length; i++) { sn = rule.style[i]; let isImportant = false; if ((rule.style.cssText.split(sn)[1] + ";").split(";")[0].indexOf("!important") > 0) { isImportant = true; } styles.push({ name: sn, cssText: rule.style[sn], isImportant: isImportant, }); } } this._cssCount = this._cssCount + 1; let loc = null; if (this._cssAst.length > 0) { let fileAst = this._cssAst.find((x) => x.file == styleSheet.href); if (fileAst) { let selectorAst = fileAst.ast.find((x) => x.name == rule.selectorText); if (selectorAst) loc = selectorAst.loc; } } matchedCSSRules.push({ selectorName: rule.selectorText, declarations: rule.style, href: styleSheet.href || '', style: styles, id: `c_${this._cssCount}`, //priority: this.#getSelectorPriority(rule.selectorText, {line:1000000-(i * 1000000 + j)}), priority: this.#getSelectorPriority(rule.selectorText, { line: i * 1000000 + j}), fileIndex: i, posIndex: j, loc:loc, }); } } } catch (error) { console.error('无法访问样式表', error); } } //根据优先级排序 return matchedCSSRules.sort((a, b) => { return CssPriority.compare(a.priority, b.priority); });; } //获取父级样式 #getParentStyle(ele) { let styleList = []; let level = 0; let pEle = ele.parentElement; while (pEle) { let inlineStyle = this.#getInlineStyle(pEle); if (inlineStyle.length > 0) { styleList.push({ relationEle: pEle, relationTag: pEle.tagName, nodeLevel: level, kind: CssKind.inline, selector: inlineStyle, }); level = level + 1; } let matchStyle = this.#getMatchStyle(pEle); if (matchStyle.length > 0) { styleList.push({ relationEle: pEle, relationTag: pEle.tagName, nodeLevel: level, kind: CssKind.match, selector: matchStyle, }); level = level + 1; } pEle = pEle.parentElement; /* if (pEle.tagName == "HTML") { pEle = null; }*/ } return styleList; } #getTopSelector(ele, selector,selectorName,line) { let find = null; for (let gItem of selectorName.split(",")) { if (!ele.matches(gItem)) continue; if (!find) { find = { selector: selector, key: gItem, priority: this.#getSelectorPriority(gItem, {line:line}), } } else { let curPriority = this.#getSelectorPriority(gItem, { line: line }); let ret = CssPriority.compare(find.priority, curPriority); if (ret > 0) { find.selector = selector; find.key = gItem; find.priority = curPriority; } } } return find; } async #getDefaultStyle(eleTag) { let tmpF = this._document.tmpF; let w = new Lock(); let tmpFStyle = null; if (!tmpF) { tmpF = this._document.createElement("iframe"); tmpF.style.display = "none"; tmpF.setAttribute("srcDoc", `<${eleTag} id="tmpf">`); this._document.body.appendChild(tmpF); this._document.tmpF = tmpF; tmpF.onload = function () { tmpFStyle = getComputedStyle(tmpF.contentWindow.document.getElementById("tmpf")); w.resolve(tmpFStyle); } } else { tmpF.setAttribute("srcDoc", `<${eleTag} id="tmpf">`); tmpF.onload = function () { tmpFStyle = getComputedStyle(tmpF.contentWindow.document.getElementById("tmpf")); w.resolve(tmpFStyle); } } return await w.wait(); } //获取非默认样式 async #getComputedStyleProp(ele) { let ret = []; //获取默认样式 let defaultStyle = await this.#getDefaultStyle(ele.tagName); //获取应用样式 let computeStyle = getComputedStyle(ele); for (let i = 0; i < computeStyle.length - 1; i++) { let sn = computeStyle[i]; if (computeStyle[sn] == defaultStyle[sn]) continue; ret.push({ name: sn, cssText: computeStyle[sn], }) } return ret; } //获取css的ast async getAst() { await this.#initCssAst(); return this._cssAst(); } async refers(ele, propName) { let ret = []; //初始化css的ast this.#initCssAst(); let inlineStyle = this.#getInlineStyle(ele);//获取自身的样式配置 let matchStyle = this.#getMatchStyle(ele);//获取匹配的样式配置 let parentStyle = this.#getParentStyle(ele);//获取所有父级的样式配置 let computeStyleProps = await this.#getComputedStyleProp(ele);//获取浏览器的非默认样式属性 //插入内联样式 if (inlineStyle.length > 0) { ret.push({ kind: CssKind.inline, selector: inlineStyle.selector, parent: null, }) } //插入匹配样式 for (let matchItem of matchStyle) { ret.push({ kind: CssKind.match, selector: matchItem, parent:null, }) } let inheritedRelation = []; //分析继承样式 for (let sty of computeStyleProps) { if (!this._inheritStyles.has(sty.name)) continue;//非继承属性,则不需要判断父级 if (propName && sty.name != propName) continue; //遍历多级父级 for (let psty of parentStyle) { if (psty.selector?.length <= 0) continue; //遍历父级中的选择器 for (let selectorItem of psty.selector) { if (selectorItem.style.length <= 0) continue; let findProp = false; //遍历选择器的样式属性 for (let pro of selectorItem.style) { if (pro.name == sty.name) { findProp = true; break; } } //找到了对应属性 if (findProp) { let findSelector = inheritedRelation.find((x) => x.selector.id == selectorItem.id); if (!findSelector) { //选择器不存在,则插入 inheritedRelation.push({ kind: CssKind.parent, selector: selectorItem, inheritedInfo: psty, }); } continue; } } } } //计算继承的优先顺序 inheritedRelation = inheritedRelation.sort((a, b) => { if (a.inheritedInfo.nodeLevel < b.inheritedInfo.nodeLevel) { return -1; } else if (a.inheritedInfo.nodeLevel > b.inheritedInfo.nodeLevel) { return 1; } else { return 0 } }); ret = ret.concat(inheritedRelation); let propApplyRelation = []; //计算属性的匹配位置 for (let prop of computeStyleProps) { if (propName && prop.name != propName) continue; let findSelector = null; let isImportant = false; //遍历关联的选择器 for (let selector of ret) { //{kind:"",selector:{},parent:null } let isFindProp = false; //遍历样式属性 for (let sty of selector.selector.style) { if (prop.name == sty.name) { isFindProp = true; //判断是否配置了important申明 if (sty.isImportant) { isImportant = true; } break; } } //找到了对应的属性 if (isFindProp) { if (!findSelector) { findSelector = selector; } else { if (selector.kind != CssKind.parent) { //important>inline>match -parent继承类中的important不会应用到子级元素 if (isImportant) { //发现important定义则结束当前属性所在选择器的遍历 findSelector = selector; break; } } else { //优先级inline>match>parent if (findSelector.kind < selector.kind) continue; } //处理kind相等的情况 if (findSelector.selector.selectorName.indexOf(",") > 0 || selector.selector.selectorName.indexOf(",") > 0) { //判断逗号之间的选择器的优先级 let findSelectorTop = this.#getTopSelector(ele, findSelector.selector, findSelector.selector.selectorName, findSelector.selector.priority[5]); let backupSelectorTop = this.#getTopSelector(ele, selector.selector, selector.selector.selectorName, selector.selector.priority[5]); let curCompare = CssPriority.compare(findSelectorTop.priority, backupSelectorTop.priority); if (curCompare > 0) { findSelector = selector; } } } } } if (findSelector) { propApplyRelation.push({ name: prop.name, cssText: prop.cssText, selector: findSelector, } ); } } return { applyRelation: propApplyRelation, applySelectors: ret, }; } }