从零开始:JS实现CSS文本的选择器解析与HTML的节点解析

【需求】

替除css 文件中失效的样式

【实现分析】

  1. 首先要找到失效的样式
  2. 需要分析什么样的样式是失效多余的 (CSS中的选择器 和HTML节点不匹配)
  3. 找到所有CSS选择器
  4. 找到所有XML 节点选择器相关的信息 标签名 id class  属性 兄弟信息 子节点信息

先贴上github地址https://github.com/yyccmmkk/js-xml-css-parser

【实施: css 选择器解析】

CSS文件读取后也是文本字符串,那么可以用正则来匹配出所有选择器;但是正则也不是一下到位,还是要分步骤:

  1. 根据CSS的书写规则,首选切分 换行 \n 与 大花括号 “{}” 之前的字符串,拿到初步的选择器信息,它们可能是这样的,后面步骤再处理,先说切分的事情,通过正则的 exec 方法循环匹配,正则可以这样写
    const regExpCssSelectorSplit = /([^{}\n]+){([^{}]+)}/g;
  2. 初步切分的CSS选择器,包含逗号"," 所以还要再次切分 这回可以简单的 调用字符串的split方法实现

  3. 通过以上两步拿到选择器可能是一个或两个或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 
  4. 记录选择器的位置信息,比如 .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; // 替换&gt;&lt;
    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>

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值