【需求】
替除css 文件中失效的样式
【实现分析】
- 首先要找到失效的样式
- 需要分析什么样的样式是失效多余的 (CSS中的选择器 和HTML节点不匹配)
- 找到所有CSS选择器
- 找到所有XML 节点选择器相关的信息 标签名 id class 属性 兄弟信息 子节点信息
先贴上github地址https://github.com/yyccmmkk/js-xml-css-parser
【实施: css 选择器解析】
CSS文件读取后也是文本字符串,那么可以用正则来匹配出所有选择器;但是正则也不是一下到位,还是要分步骤:
- 根据CSS的书写规则,首选切分 换行 \n 与 大花括号 “{}” 之前的字符串,拿到初步的选择器信息,它们可能是这样的,后面步骤再处理,先说切分的事情,通过正则的 exec 方法循环匹配,正则可以这样写
const regExpCssSelectorSplit = /([^{}\n]+){([^{}]+)}/g;
-
初步切分的CSS选择器,包含逗号"," 所以还要再次切分 这回可以简单的 调用字符串的split方法实现
-
通过以上两步拿到选择器可能是一个或两个或N个的组合,需要再次切分
div div.xxxx.as p:first-of-type button[type^="but"] div[lang="en"]:host > div.a.b .c + p ~ img > span[type="as"]:hover div[lang="en"]:host>div.a.b .c+p~img>span[type="as"]:hover
div.xxxxx.as 拆分为 div .xxxxx .as 三个选择器 放到一个数组里待用,那像 > + :hover 这样的单独拆解成 +p > sapn ~ img 空格.c (注意空格标示它是后代关系),经过拆解后的 子选择器 大致是这样的 第一位是操作符比如# . + > : 空格(表示后代选择);如下图所示是最终想要的结果
实际的情况远比这个复杂,你就不能估算中间会有多少个空格
div[lang="en"]:host > div.a.b .c + p ~ img > span[type="as"]:hover {
所以先把多余的空格变成一个,把 > + ~ 前后的空格剔除 ,使用正则替换
const regExpEmpty = /\s+([>+~])\s+/g; const regExpRepeatSpace = /\s{2,}/g; let tempStr = v.trim().replace(regExpRepeatSpace, '\u0020').replace(regExpEmpty, '$1');
接下来,需要写正则了,这个正则要切分出来的最终结果是这样的
['div','.demo','.demo2',' .demo3',':hover','>div',' div','+p','~img','buttom','[type="input"]']
之所以搞成这样一个数组,主要是为了方便处理,表示清楚子选择器元素和前边选择器的关系;拿前三个来说:div .demo .demo2 表示的关系是div.demo.demo2 因为 .demo 和.demo2 前边第一位不是关系符(暂且这么叫吧,不是空格 和+>~: [); 拿第4,5,6来说,4 第一位是空格表示的是后代选择器,5表示的是伪类 6 是直接子元素,这样在后边处理的时候会方便。正则怎么写呢?贴代码(欢迎指正留言),不用正测可以吗?可以,但是要写好多代码去切分
const regExpCutToken = /[#.+>~\s:]*[^#.+>~:\s\[\]]+|\[[^\[\]]+\]/g
-
记录选择器的位置信息,比如 .demo div, .demo2 { color:red} 这条样式,有两个选择器 .demo div 和 .demo2 ,再加上样式代码{ color:red} 。首先记录这条样式的开始载止位置记为tStart 和tEnd ,方便在以后的应用中整条剔除,其次还要记录两个选择器的起始位 .demo div 和 ,.demo2 (注意 :这块加上了逗号这样假如发现没有.demo2 时,就可以只删除.demo2 这个选择器同时把多余的逗号也删了)起止位记为 delStart delEnd 选择器本身的起止位记为 sStart sEnd。(后续补图详解,今天先到这)
先贴个测试页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<style type="text/css">
* {
padding: 0;
}
html, body, div {
margin: 0;
}
div, div.demo {
border: 0px solid red;
}
div, div.xxxx {
border: 0px solid red;
}
div.xxxx {
border: 0px solid red;
}
div.xxxx.as {
border: 0px solid red;
}
div:hover {
color: red;
}
button[type="button"] {
width: 100px;
}
button[type^="but"] {
height: 30px;
}
p:first-of-type {
background: #fff;
}
div[lang="en"]:host > div.a.b .c + p ~ img > span[type="as"]:hover {
font-size: 14px;
}
div[lang="en"]:host>div.a.b .c+p~img>span[type="as"]:hover {
font-size: 14px;
}
div div p span a {
font-size: 12px;
}
div.line,
p.line2,
img {
font-size: 14px;
}
div.line,
p.line2,
img {
font-size: 14px;
}
div.line,
p.line2,
img {
font-size: 14px;
}
@media screen and (max-width: 300px) {
body {
background-color: lightblue;
}
#demo {
width: 222px;
}
}
</style>
<div class=" demo " id="x">
<div class="same dk" id="kx">11
<img src="{{src}}"/>
<p><span class="demo2"></span></p>
<div>3</div>
</div>
<div class=" same s7 tr">
<h5 class="demo3">1</h5>
</div>
<div>
<div class="same">
<p class="m2">
<span class="same">5</span>
</p>
</div>
</div>
</div>
<script>
{
const regExp = /<([^>\s]+)([^>]*)(\/?)>/g; // 解析节点
const regExpReplace = /{{[^{}]*}}/g; // 替换><
const regExpClass = /class\s*=\s*["']?([^"']+)/; // className
const regExpId = /id\s*=\s*["']?([^"']+)/; // id
const regExpBr = /[\r\n]/g; // 换行
const regExpRepeatSpace = /\s{2,}/g;
const regExpCutToken = /[#.+>~\s:]*[^#.+>~:\s\[\]]+|\[[^\[\]]+\]/g || /[#.+>~]?[^#.+>~\[\]]+|\[[^\[\]]+\]/g; //todo 有意思
const regExpCssSelectorSplit = /([^{}\n]+){([^{}]+)}/g;
const regExpCssStart = /\S([\s\S]+)\S/;
const regExpEmpty = /\s+([>+~])\s+/g;
const regExpIsTag = /^[^#.+>~]+$/;
const regExpComment = /<!--[\s\S]+?-->/g;
const str = document.querySelector('.demo').outerHTML;
const str1 = str.replace(/[\r\n]+/g, '');
const style = document.querySelector('style').innerHTML;
const closeList = ['img'];
const ignoreList = ['*', 'html', 'body', 'page'];
console.log(str);
console.log(style)
const tree;
class parser {
constructor(opt) {
this.cache = {}
}
transformHtml(str) {
let result;
let tree = {
tagName: 'root',
parent: null,
children: []
};
let deep = 0;
let cur = tree;
let tagName;
let count = 1;
let nodeMap = {};
let classNameMap = {};
let idMap = {};
let tagMap = {};
str = str.replace(regExpReplace, '');
str = str.replace(regExpComment, '');
while ((result = regExp.exec(str)) !== null) {
tagName = result[1];
if (tagName) {
if (tagName.charAt(0) === '/') {
deep--;
cur = cur.parent;
} else {
let attr = result[2].trim();
let isClose = result[3] === '/' || closeList.includes(tagName);
!isClose && deep++;
let parent = cur;
let tempKey = `key${count++}`;
let id;
let className;
if (attr) {
id = attr.match(regExpId);
id = id && id[1].trim() || null;
className = attr.match(regExpClass);
className = className && className[1].trim() || null;
}
cur = {
parent,
children: [],
tagName,
id,
className,
attributes: [],
selector: [],
key: tempKey,
position: result.index
};
parent.children.push(cur);
nodeMap[tempKey] = cur;
tagMap[tagName] ? tagMap[tagName].push(tempKey) : tagMap[tagName] = [tempKey];
if (className) {
let temp = className.split('\u0020');
for (let v of temp) {
classNameMap[v] ? classNameMap[v].push(tempKey) : classNameMap[v] = [tempKey];
}
}
if (id) {
idMap[id] ? idMap[id].push(tempKey) : idMap[id] = [tempKey];
}
if (isClose) {
cur = parent;
}
}
}
}
return {
tree,
nodeMap,
tagMap,
idMap,
classNameMap,
deep
}
}
transformCss(str) {
let result;
let selectors = [];
while ((result = regExpCssSelectorSplit.exec(str)) !== null) {
//console.log(result, ":::::::::result");
let tStart = result.index;
let tEnd = result.index + result[0].length;
let selector = result[1].replace(regExpBr, '').trim();
let tempMatch = result[1].match(regExpCssStart);
let tempOffset = tempMatch && tempMatch.index || 0;
let sStart = result.index + tempOffset;
let sEnd = sStart + (tempMatch && tempMatch[0].length || 0);
selectors.push({
selector,
tStart,
tEnd,
sStart,
sEnd
})
}
let list = [];
for (let v of selectors) {
const {selector, tStart, tEnd, sStart, sEnd} = v;
let tempList = selector.split(',');
let count = 0;
let delStart = sStart;
for (let v of tempList) {
count += v.length + 1;
let tempStr = v.trim().replace(regExpRepeatSpace, '\u0020').replace(regExpEmpty, '$1');
let selector = tempStr.match(regExpCutToken);
let tempCount = sStart + count - 1;
let tempStart = sStart + (count - v.length - 1);
let line = this.findLineNo(str, tempStart);
list.push(selector.map(v => ({
selector: v, // 子子级选择器,
tStart, // 整条css 样式起始位
tEnd, // 整条css 样式截止位
sStart, // 整个选择器开始位
sEnd, // 整个选择器结束位
start: tempStart,// 子选择器开始位
end: tempCount, // 子选择器截止位
delStart, // 子选择器删除起始位,包含选择器前逗号
delEnd: tempCount, // 子选择器删除截止位
line
})));
delStart = tempCount
}
}
return list;
}
check(html, css, js) {
let {tree, nodeMap, idMap, classNameMap, tagMap, deep} = this.transformHtml(html);
let selectors = this.transformCss(css);
let result = this.checkSelector(selectors, tree, nodeMap, idMap, classNameMap, tagMap);
console.log(result, 'result::');
console.log('tree::', tree, '\r\nnodeMap::', nodeMap, '\r\ndeep::', deep, '\r\nselectors::', selectors, '\r\nidMap::', idMap, '\r\nclassNameMap::', classNameMap, '\r\ntag::', tagMap);
}
isHasSelector(map, selector) {
selector = selector.trim();
let letter = selector.charAt(0);
let key = letter === '#' ? 'idMap' : letter === '.' ? 'classNameMap' : 'tagMap';
let tempSelector = regExpIsTag.test(selector) ? selector : selector.slice(1);
return Object.keys(map[key]).includes(tempSelector.trim());
}
checkSelector(selectors, tree, nodeMap, idMap, classNameMap, tagMap) {
let result = [];
debugger
for (let v of selectors) {
for (let i = 0, len = v.length; i < len; i++) {
let item = v[i];
let {selector} = item;
let relationalSymbols = selector.charAt(0);
if (ignoreList.includes(selector.trim()) || relationalSymbols === ':') { // 过滤白名单 伪类
continue;
}
if (!this.isHasSelector({idMap, classNameMap, tagMap}, selector)) {
result.push(item);
break
} else if (relationalSymbols === '#') {
// todo check
} else if (relationalSymbols === '.') {
// todo check
} else if (relationalSymbols === '+') {
// todo check
} else if (relationalSymbols === '~') {
// todo check
} else if (relationalSymbols === '>') {
// todo check
}
}
}
return result
}
findLineNo(str, end) {
return str.slice(0, end).split('\n').length;
}
}
const p = new parser();
p.check(str1, style);
}
</script>
</body>
</html>