美文网首页
自己动手编写stylelint规则

自己动手编写stylelint规则

作者: Jtag特工 | 来源:发表于2021-12-01 22:14 被阅读0次

    自己动手编写stylelint规则

    之前我们介绍了如何编写eslint规则,现在我们开始挑战更偏前端的新领域 - stylelint.

    从运行测试用例入手

    按照惯例,我们还是从测试用例入手。测试用例是将css代码和规则快速运行起来的最有效手段。

    首先我们把stylelint的代码拉下来:

    git clone https://github.com/stylelint/stylelint
    

    好,我们下面找个例子看看stylelint的测试用例是什么样的。我们以对于颜色的检查的规则为例,这个规则是检查颜色的16进制值是不是正确的,比如#000,#000000都是正确的,#0000就是错误的,这个大家都清楚哈:

    const { messages, ruleName } = require('..');
    
    testRule({
        ruleName,
        config: [true],
    
        accept: [
            {
                code: 'a { color: pink; }',
            },
            {
                code: 'a { color: #000; }',
            },
            {
                code: 'a { something: #000, #fff, #ababab; }',
            },
    ...
        ],
    
        reject: [
            {
                code: 'a { color: #ababa; }',
                message: messages.rejected('#ababa'),
                line: 1,
                column: 12,
            },
            {
                code: 'a { something: #00, #fff, #ababab; }',
                message: messages.rejected('#00'),
                line: 1,
                column: 16,
            },
            {
                code: 'a { something: #000, #fff1az, #ababab; }',
                message: messages.rejected('#fff1az'),
                line: 1,
                column: 22,
            },
            {
                code: 'a { something:#000,#fff,#12345aa; }',
                message: messages.rejected('#12345aa'),
                line: 1,
                column: 25,
            },
        ],
    });
    

    accept数组是可以通过的case,reject是失败的用例,还要给出理由和错误位置。

    stylelint的测试用例是用jest测试框架写的,运行使用jest:

    ./node_modules/jest/bin/jest.js lib/rules/color-no-invalid-hex/__tests__/index.js
    

    运行结果如下:

     PASS  lib/rules/color-no-invalid-hex/__tests__/index.js
      color-no-invalid-hex
        accept
          [ true ]
            'a { color: pink; }'
              ✓ no description (26 ms)
            'a { color: #000; }'
              ✓ no description (1 ms)
            'a { something: #000, #fff, #ababab; }'
              ✓ no description (1 ms)
            'a { color: #0000ffcc; }'
              ✓ eight digits (2 ms)
            'a { color:#00fc; }'
              ✓ four digits (2 ms)
            'a { padding: 000; }'
              ✓ no description (1 ms)
            'a::before { content: "#ababa"; }'
              ✓ no description (1 ms)
            "a { background-image: svg-load('x.svg', fill=url(#a)); }"
              ✓ svg-load url with fill (2 ms)
            'a { background-image: url(#a); }'
              ✓ url standalone hash (2 ms)
            'a { background-image: url(x.svg#a); }'
              ✓ url with hash (1 ms)
            '@font-face {\n' +
      'font-family: dashicons;\n' +
      'src: url(data:application/font-woff;charset=utf-8;base64, ABCDEF==) format("woff"),\n' +
      'url(../fonts/dashicons.ttf) format("truetype"),\n' +
      'url(../fonts/dashicons.svg#dashicons) format("svg");\n' +
      'font-weight: normal;\n' +
      'font-style: normal;\n' +
      '}'
              ✓ no description (3 ms)
            'a { color: #colors[somecolor]; }'
              ✓ Less map usage (11 ms)
            'a { border-#$side: 0; }'
              ✓ ignore sass-like interpolation (7 ms)
            'a { box-sizing: #$type-box; }'
              ✓ ignore sass-like interpolation (1 ms)
            'export default <h1 style={{ color: "#ffff" }}>Test</h1>;'
              ○ skipped no description
        reject
          [ true ]
            'a { color: #ababa; }'
              ✓ no description (2 ms)
            'a { something: #00, #fff, #ababab; }'
              ✓ no description (2 ms)
            'a { something: #000, #fff1az, #ababab; }'
              ✓ no description (2 ms)
            'a { something:#000,#fff,#12345aa; }'
              ✓ no description (1 ms)
            'export default <h1 style={{ color: "#fffff" }}>Test</h1>;'
              ○ skipped no description
    
    Test Suites: 1 passed, 1 total
    Tests:       2 skipped, 18 passed, 20 total
    Snapshots:   0 total
    Time:        0.635 s, estimated 1 s
    Ran all test suites matching /lib\/rules\/color-no-invalid-hex\/__tests__\/index.js/i.
    

    规则的内容我稍删节一点细节,大致是下面这样:

    ...
    const valueParser = require('postcss-value-parser');
    
    const ruleName = 'color-no-invalid-hex';
    
    const messages = ruleMessages(ruleName, {
        rejected: (hex) => `Unexpected invalid hex color "${hex}"`,
    });
    
    ...
    const rule = (primary) => {
        return (root, result) => {
    ...
            root.walkDecls((decl) => {
                if (!isStandardSyntaxHexColor(decl.value)) {
                    return;
                }
    
                valueParser(decl.value).walk(({ value, type, sourceIndex }) => {
                    if (type === 'function' && value.endsWith('url')) return false;
    
                    if (type !== 'word') return;
    
                    const hexMatch = /^#[0-9A-Za-z]+/.exec(value);
    
                    if (!hexMatch) return;
    
                    const hexValue = hexMatch[0];
    
                    if (isValidHex(hexValue)) return;
    
                    report({
                        message: messages.rejected(hexValue),
                        node: decl,
                        index: declarationValueIndex(decl) + sourceIndex,
                        result,
                        ruleName,
                    });
                });
            });
        };
    };
    
    ...
    

    如何遍历声明

    css基本上都是一些声明,我们都过root.walkDecls去遍历它们。

    比如对于

    a { color: pink; }
    

    来说,decl.value的值就是pink.

    对于有多个值的,例如:

    a { something: #000, #fff, #ababab; }
    

    Decl.value值就是"#000, #fff, #ababab"

    这时候的value就需要进一步拆分,就是valueParser(decl.value).walk的作用。

    valueParser.walk的参数是值、类型、源代码索引的列表。

    比如"pink"的类型是word, "#000"也是word, ","是div, "svg-load"是function, "x.svg"是string等。

    类型一共有7种: word, string, div, space, comment, function和unicode-range.

    image

    报错信息仍然和eslint一样,是通过report接口来实现的。

    再看一个完整例子

    我们再看一个简单例子,禁止使用"!important"属性。

    const report = require('../../utils/report');
    const ruleMessages = require('../../utils/ruleMessages');
    const validateOptions = require('../../utils/validateOptions');
    
    const ruleName = 'declaration-no-important';
    
    const messages = ruleMessages(ruleName, {
        rejected: 'Unexpected !important',
    });
    
    const rule = (primary) => {
        return (root, result) => {
            const validOptions = validateOptions(result, ruleName, { actual: primary });
    
            if (!validOptions) {
                return;
            }
    
            root.walkDecls((decl) => {
                if (!decl.important) {
                    return;
                }
    
                report({
                    message: messages.rejected,
                    node: decl,
                    word: 'important',
                    result,
                    ruleName,
                });
            });
        };
    };
    
    rule.ruleName = ruleName;
    rule.messages = messages;
    module.exports = rule;
    

    因为important是decl的属性,解析器已经替我们搞好了,我们只要判断这一个属性即可。

    Postcss-value-parser解析器

    上面我们使用的valueParser是postcss-value-parser解析器。

    我们来看看如何单独使用postcss-value-parser去解析css代码,其实非常简单,只要一个valueParser就可以了:

    const valueParser = require('postcss-value-parser');
    
    const hellocss1 = `
    #hellocss {
        background-color: blue;
    }
    `
    
    const parsedCss = valueParser(hellocss1);
    console.log(parsedCss);
    

    解析出来的结果如下:

    ValueParser {
      nodes: [
        { type: 'space', sourceIndex: 0, sourceEndIndex: 1, value: '\n' },
        {
          type: 'word',
          sourceIndex: 1,
          sourceEndIndex: 10,
          value: '#hellocss'
        },
        { type: 'space', sourceIndex: 10, sourceEndIndex: 11, value: ' ' },
        { type: 'word', sourceIndex: 11, sourceEndIndex: 12, value: '{' },
        {
          type: 'space',
          sourceIndex: 12,
          sourceEndIndex: 17,
          value: '\n    '
        },
        {
          type: 'word',
          sourceIndex: 17,
          sourceEndIndex: 33,
          value: 'background-color'
        },
        {
          type: 'div',
          sourceIndex: 33,
          sourceEndIndex: 35,
          value: ':',
          before: '',
          after: ' '
        },
        {
          type: 'word',
          sourceIndex: 35,
          sourceEndIndex: 40,
          value: 'blue;'
        },
        { type: 'space', sourceIndex: 40, sourceEndIndex: 41, value: '\n' },
        { type: 'word', sourceIndex: 41, sourceEndIndex: 42, value: '}' },
        { type: 'space', sourceIndex: 42, sourceEndIndex: 43, value: '\n' }
      ]
    }
    

    要去进一步处理ValueParser解析出来的数据,可以通过walk函数来进一步处理:

    const valueParser = require('postcss-value-parser');
    
    const hellocss1 = `
    #hellocss {
        width: 20px;
    }
    `
    
    const parsedCss = valueParser(hellocss1);
    
    parsedCss.walk((node) => {
        console.log(node.type, node.value, node.sourceIndex);
    });
    

    输出的结果如下:

    space 
     0
    word #hellocss 1
    space   10
    word { 11
    space 
         12
    word width 17
    div : 22
    word 20px; 24
    space 
     29
    word } 30
    space 
     31
    

    解析单位

    css中比起javascript来,有一个特有的问题,就是很多属性是带单位的,比如px, rpx, em, rem等。将值解析成数字和单位是非常通用的需求。valueParser为我们提供了unit函数来实现这个功能:

    const s1 = '20px';
    const value1 = valueParser.unit(s1);
    console.log(value1);
    

    选择器宇宙

    这部分看起来有点复杂,因为涉及到的层次比较多。请大家稍耐心一点点,要不然解析的时候就找不到对象了。

    单项选择器

    普通属性处理完之后,我们需要专门设一节来说选择器,postcss有一个专门的库postcss-selector-parser来处理选择器。

    选择器的特点就是属性多,有各种复杂组合。

    我们从最基础的看起。

    const parser = require('postcss-selector-parser');
    const transform = selectors => {
        selectors.walk(selector => {
            console.log(selector.type)
        });
    };
    
    parser(transform).processSync('p');
    

    输出为:

    selector
    tag
    

    说明p是tag selector。

    我们可以打印完整的selector对象的结构来看下:

    const parser = require('postcss-selector-parser');
    const transform = selectors => {
        selectors.walk(selector => {
            console.log(selector)
        });
    };
    
    parser(transform).processSync('p');
    
    

    输出如下:

    <ref *1> Selector {
      source: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
      spaces: { before: '', after: '' },
      nodes: [
        Tag {
          value: 'p',
          source: [Object],
          sourceIndex: 0,
          spaces: [Object],
          type: 'tag',
          parent: [Circular *1]
        }
      ],
      type: 'selector',
      parent: Root {
        source: { start: [Object], end: [Object] },
        spaces: { before: '', after: '' },
        nodes: [ [Circular *1] ],
        type: 'root',
        _error: [Function (anonymous)],
        lastEach: 1,
        indexes: { '1': 0 }
      }
    }
    <ref *1> Tag {
      value: 'p',
      source: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
      sourceIndex: 0,
      spaces: { before: '', after: '' },
      type: 'tag',
      parent: Selector {
        source: { start: [Object], end: [Object] },
        spaces: { before: '', after: '' },
        nodes: [ [Circular *1] ],
        type: 'selector',
        parent: Root {
          source: [Object],
          spaces: [Object],
          nodes: [Array],
          type: 'root',
          _error: [Function (anonymous)],
          lastEach: 1,
          indexes: [Object]
        },
        lastEach: 1,
        indexes: { '1': 0 }
      }
    }
    

    我们再看下类选择器、ID选择器和*:

    parser(transform).processSync('.class1');
    parser(transform).processSync('#id1');
    parser(transform).processSync('*');
    

    类选择器的类型是ClassName:

    ...
    <ref *1> ClassName {
      _value: 'class1',
      source: { start: { line: 1, column: 1 }, end: { line: 1, column: 7 } },
      sourceIndex: 0,
      spaces: { before: '', after: '' },
      type: 'class',
      _constructed: true,
      parent: Selector {
        source: { start: [Object], end: [Object] },
        spaces: { before: '', after: '' },
        nodes: [ [Circular *1] ],
        type: 'selector',
        parent: Root {
    ...
        },
        lastEach: 1,
        indexes: { '1': 0 }
      }
    }
    

    ID选择器的类型是ID:

    ...
    <ref *1> ID {
      value: 'id1',
      source: { start: { line: 1, column: 1 }, end: { line: 1, column: 4 } },
      sourceIndex: 0,
      spaces: { before: '', after: '' },
      type: 'id',
      parent: Selector {
        source: { start: [Object], end: [Object] },
        spaces: { before: '', after: '' },
        nodes: [ [Circular *1] ],
        type: 'selector',
        parent: Root {
    ...
        },
        lastEach: 1,
        indexes: { '1': 0 }
      }
    }
    

    *选择器的类型是Universal:

    ...
    <ref *1> Universal {
      value: '*',
      source: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
      sourceIndex: 0,
      spaces: { before: '', after: '' },
      type: 'universal',
      parent: Selector {
        source: { start: [Object], end: [Object] },
        spaces: { before: '', after: '' },
        nodes: [ [Circular *1] ],
        type: 'selector',
        parent: Root {
    ...
        },
        lastEach: 1,
        indexes: { '1': 0 }
      }
    }
    

    伪类选择器

    我们先看一个独立的伪类选择器:root

    <ref *1> Selector {
      source: { start: { line: 1, column: 1 }, end: { line: 1, column: 5 } },
      spaces: { before: '', after: '' },
      nodes: [
        Pseudo {
          value: ':root',
          source: [Object],
          sourceIndex: 0,
          spaces: [Object],
          nodes: [],
          type: 'pseudo',
          parent: [Circular *1]
        }
      ],
      type: 'selector',
      parent: Root {
    ...
      }
    }
    

    再来一个div::after,两个标签组合的:

    parser(transform).processSync('div::after');
    

    它就是Nodes中包含了两个选择器:

    <ref *1> Selector {
      source: { start: { line: 1, column: 1 }, end: { line: 1, column: 10 } },
      spaces: { before: '', after: '' },
      nodes: [
        Tag {
          value: 'div',
          source: [Object],
          sourceIndex: 0,
          spaces: [Object],
          type: 'tag',
          parent: [Circular *1]
        },
        Pseudo {
          value: '::after',
          source: [Object],
          sourceIndex: 3,
          spaces: [Object],
          nodes: [],
          type: 'pseudo',
          parent: [Circular *1]
        }
      ],
      type: 'selector',
      parent: Root {
        source: { start: [Object], end: [Object] },
        spaces: { before: '', after: '' },
        nodes: [ [Circular *1] ],
        type: 'root',
        _error: [Function (anonymous)],
        lastEach: 1,
        indexes: { '1': 0 }
      }
    }
    

    下面高亮的时刻来了,我们来看看p:nth-child(4)

    parser(transform).processSync('p:nth-child(4)');
    

    它的层次是下面这样的:

    • selector
      • tag
      • pseudo
        • selector
          • tag

    详情如下:

    <ref *1> Selector {
      source: { start: { line: 1, column: 1 }, end: { line: 1, column: 14 } },
      spaces: { before: '', after: '' },
      nodes: [
        Tag {
          value: 'p',
          source: [Object],
          sourceIndex: 0,
          spaces: [Object],
          type: 'tag',
          parent: [Circular *1]
        },
        Pseudo {
          value: ':nth-child',
          source: [Object],
          sourceIndex: 1,
          spaces: [Object],
          nodes: [Array],
          type: 'pseudo',
          parent: [Circular *1]
        }
      ],
      type: 'selector',
      parent: Root {
    ...
      }
    }
    <ref *1> Tag {
      value: 'p',
      source: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
      sourceIndex: 0,
      spaces: { before: '', after: '' },
      type: 'tag',
      parent: Selector {
        source: { start: [Object], end: [Object] },
        spaces: { before: '', after: '' },
        nodes: [ [Circular *1], [Pseudo] ],
        type: 'selector',
        parent: Root {
    ...
        },
        lastEach: 1,
        indexes: { '1': 0 }
      }
    }
    <ref *1> Pseudo {
      value: ':nth-child',
      source: { start: { line: 1, column: 2 }, end: { line: 1, column: 14 } },
      sourceIndex: 1,
      spaces: { before: '', after: '' },
      nodes: [
        Selector {
          source: [Object],
          spaces: [Object],
          nodes: [Array],
          type: 'selector',
          parent: [Circular *1]
        }
      ],
      type: 'pseudo',
      parent: Selector {
        source: { start: [Object], end: [Object] },
        spaces: { before: '', after: '' },
        nodes: [ [Tag], [Circular *1] ],
        type: 'selector',
        parent: Root {
    ...
        },
        lastEach: 1,
        indexes: { '1': 1 }
      }
    }
    <ref *1> Selector {
      source: { start: { line: 1, column: 12 }, end: { line: 1, column: 14 } },
      spaces: { before: '', after: '' },
      nodes: [
        Tag {
          value: '4',
    ...
          type: 'tag',
          parent: [Circular *1]
        }
      ],
      type: 'selector',
      parent: Pseudo {
        value: ':nth-child',
        source: { start: [Object], end: [Object] },
        sourceIndex: 1,
        spaces: { before: '', after: '' },
        nodes: [ [Circular *1] ],
        type: 'pseudo',
        parent: Selector {
    ...
        },
        lastEach: 1,
        indexes: { '1': 0 }
      }
    }
    <ref *1> Tag {
      value: '4',
      source: { start: { line: 1, column: 13 }, end: { line: 1, column: 13 } },
      sourceIndex: 12,
      spaces: { before: '', after: '' },
      type: 'tag',
      parent: Selector {
        source: { start: [Object], end: [Object] },
        spaces: { before: '', after: '' },
        nodes: [ [Circular *1] ],
        type: 'selector',
        parent: Pseudo {
          value: ':nth-child',
    ...
        },
        lastEach: 1,
        indexes: { '1': 0 }
      }
    }
    
    

    选择器的组合

    这个组合是指引入了">", "+"这样的Combinator的组合,它将引进一个Combinator

    <ref *1> Selector {
      source: { start: { line: 1, column: 1 }, end: { line: 1, column: 7 } },
      spaces: { before: '', after: '' },
      nodes: [
        Tag {
          value: 'div',
          source: [Object],
          sourceIndex: 0,
          spaces: [Object],
          type: 'tag',
          parent: [Circular *1]
        },
        Combinator {
          value: '>',
          source: [Object],
          sourceIndex: 4,
          spaces: [Object],
          type: 'combinator',
          raws: [Object],
          parent: [Circular *1]
        },
        Tag {
          value: 'p',
          source: [Object],
          sourceIndex: 6,
          spaces: [Object],
          type: 'tag',
          parent: [Circular *1]
        }
      ],
      type: 'selector',
      parent: Root {
    ...
      }
    }
    ...
    

    属性选择器

    最后我们看下属性选择器,这个没什么特别的,只是一个Attribute类型而己:

    parser(transform).processSync('a[href^="https"]');
    

    输出如下:

    <ref *1> Selector {
      source: { start: { line: 1, column: 1 }, end: { line: 1, column: 16 } },
      spaces: { before: '', after: '' },
      nodes: [
        Tag {
          value: 'a',
          source: [Object],
          sourceIndex: 0,
          spaces: [Object],
          type: 'tag',
          parent: [Circular *1]
        },
        Attribute {
          source: [Object],
          sourceIndex: 1,
          _attribute: 'href',
          operator: '^=',
          _value: 'https',
          _quoteMark: '"',
          raws: [Object],
          spaces: [Object],
          type: 'attribute',
          _constructed: true,
          parent: [Circular *1]
        }
      ],
      type: 'selector',
      parent: Root {
    ...
      }
    }
    ...
    

    在sytlelint代码中的应用

    在StyleLint中,postcss-selector-parser的功能被封装成parseSelector函数,其实就是processSync的封装:

    const selectorParser = require('postcss-selector-parser');
    
    module.exports = function parseSelector(selector, result, node, callback) {
        try {
            return selectorParser(callback).processSync(selector);
        } catch {
            result.warn('Cannot parse selector', { node, stylelintType: 'parseError' });
        }
    };
    

    下面举一个要求id选择器的名字必须符合某个pattern的规则的实现:

    function rule(pattern) {
        return (root, result) => {
    ...
    
            const normalizedPattern = isString(pattern) ? new RegExp(pattern) : pattern;
    
            root.walkRules((ruleNode) => {
                if (!isStandardSyntaxRule(ruleNode)) {
                    return;
                }
    
                const selector = ruleNode.selector;
    
                parseSelector(selector, result, ruleNode, (fullSelector) => {
                    fullSelector.walk((selectorNode) => {
                        if (selectorNode.type !== 'id') {
                            return;
                        }
    
                        const value = selectorNode.value;
                        const sourceIndex = selectorNode.sourceIndex;
    
                        if (normalizedPattern.test(value)) {
                            return;
                        }
    
                        report({
                            result,
                            ruleName,
                            message: messages.expected(value, pattern),
                            node: ruleNode,
                            index: sourceIndex,
                        });
                    });
                });
            });
        };
    }
    

    小结

    本文我们学习了StyleLint的基本框架,用于分析css值的valueParser和用于选择器的parseSelector。

    最后再补充一点,keywordSets中包括了已知的主要关键字,我们进行有效性判断时可以充分利用。

    比如长度单位:

    keywordSets.lengthUnits = new Set([
        // Relative length units
        'em',
        'ex',
        'ch',
        'rem',
        'rlh',
        'lh',
        // Viewport-percentage lengths
        'vh',
        'vw',
        'vmin',
        'vmax',
        'vm',
        // Absolute length units
        'px',
        'mm',
        'cm',
        'in',
        'pt',
        'pc',
        'q',
        'mozmm',
        // Flexible length units
        'fr',
    ]);
    

    动画速度参数:

    keywordSets.animationTimingFunctionKeywords = uniteSets(keywordSets.basicKeywords, [
        'linear',
        'ease',
        'ease-in',
        'ease-in-out',
        'ease-out',
        'step-start',
        'step-end',
        'steps',
        'cubic-bezier',
    ]);
    

    等等

    相关文章

      网友评论

          本文标题:自己动手编写stylelint规则

          本文链接:https://www.haomeiwen.com/subject/eayexrtx.html