美文网首页
听说你想写个渲染引擎 - css 解析

听说你想写个渲染引擎 - css 解析

作者: 微微笑的蜗牛 | 来源:发表于2021-04-08 14:09 被阅读0次

    大家好,我是微微笑的蜗牛,🐌。

    上篇文章中,我们讲述了 html 的解析,并实现了一个小小的 html 解析器。没看过的同学可以戳下面链接先回过头去看看。

    今天,主要讲解 css 的解析,同样会实现一个简单的 css 解析器,输出样式表。

    css 规则

    css 的规则有些复杂,除了基本的通用选择器、元素选择器、类选择器、ID 选择器外,还有分组选择器,组合选择器等。

    • 通用选择器,* 为通配符,表示匹配任意元素。
    * {
        width: 100px;
    }
    
    • 元素选择器,定义标签的样式。
    // 任何 div 元素都匹配该样式
    div {
        width: 100px;
    }
    
    • ID 选择器,以 # 开头,元素中使用 id 属性指定。
    // id 为 test 的元素都可匹配
    #test {
        text-align: center;
    }
    
    // 设置 id
    <span id="test"></span>
    <h1 id="test"></h1>
    

    另外,它还可跟元素进行组合,表示双重匹配。

    // 表示当为 h1 标签且 id = test 才进行匹配
    h1#test {
        text-align: center;
        color: #ffffff;
    }
    
    <h1 id="test"></h1>
    
    • 类选择器,以 . 开头,元素中使用 class 属性指定。
    .test {
        height: 200px;
    }
    
    // 匹配
    <div class="test"></div>
    <p class="test"></p>
    

    同样,它也可以跟元素进行组合,双重匹配。这样一来,只有当元素相同,且元素的 class 属性包含规则中指定的全部 class 时,才会匹配。

    div.test.test1 {
        height: 200px;
    }
    
    // 匹配
    <div class="test test1"></div>
    <div class="test test1 test2"></div>
    
    // 不匹配
    <div class="test test2"></div>
    
    • 分组选择器,指定一组选择器,以 , 隔开。节点满足任意一个选择器即可匹配样式。
    div.test, #main {
        height: 200px;
    }
    
    • 组合选择器,有多种组合方式,这里就不展开说了。

    实现目标

    为了简单起见,我们只实现上面提到的几种选择器:通用选择器、元素选择器、类选择器、ID 选择器外、分组选择器。

    除此之外,选择器还存在优先级。优先级如下:

    ID 选择器 > 类选择器 > 元素选择器

    对于属性值来说,可以有多种表示方式,比如:

    • 关键字,即满足一定规则的纯字符串,如:text-align: center;
    • 长度,有数值+单位的方式,如 height: 200px;,而单位又可有多种,em/px 等;还有百分比形式,如height: 90%;
    • 色值,可使用十六进制 color: #ffffff;,也可使用颜色字符串表示 color: white;
    • ...

    这里,只支持最基础的形式。

    • 关键字。
    • 长度为数值类型,且单位固定为 px
    • 色值,固定为十六进制,支持 rgba/rgb

    数据结构定义

    样式表,由 css 规则列表组成,也是 css 解析的最终产物。

    那么该如何定义数据结构,来表示 css 规则呢?

    根据上面的 css 写法,我们可以知道:

    css 规则 = 选择器列表 + 属性值列表

    其中,选择器又有元素选择器、类选择器、ID 选择器三种形式。简单来说,可包含 tag、class、id,且 class 可有多个。

    那么,对于选择器的结构来说,可定义如下:

    struct SimpleSelector {
        // 标签名
        var tagName: String?
        
        // id
        var id: String?
        
        // class
        var classes: [String]
    }
    
    // 可作为扩展,比如可添加组合选择器,现只支持简单选择器
    enum CSSSelector {
        case Simple(SimpleSelector)
    }
    

    属性结构,比较好定义。属性名+属性值。

    struct Declaration {
        let name: String
        let value: Value
    }
    

    上面说到,属性值分为三种类型:

    • 关键字
    • 色值
    • 数值长度,单位只支持 px

    因此,属性值结构定义如下:

    enum Value {
            // 关键字
        case Keyword(String)
        
        // rgba
        case Color(UInt8, UInt8, UInt8, UInt8)
        
        // 长度
        case Length(Float, Unit)
    }
    
    // 单位
    enum Unit {
        case Px
    }
    

    有了如上结构,便可定义出 css 规则的结构。

    // css 规则结构定义
    struct Rule {
        // 选择器
        let selectors: [CSSSelector]
        
        // 声明的属性
        let declarations: [Declaration]
    }
    

    同样,样式表的结构也可定义出来了。

    // 样式表,最终产物
    struct StyleSheet {
        let rules: [Rule]
    }
    

    整体数据结构如下图所示:

    image

    关于选择器优先级,通过一个三元组来区分。

    // 用于选择器排序,优先级从高到低分别是 id, class, tag
    typealias Specifity = (Int, Int, Int)
    

    排序是根据「否存在 id」、「class 个数」、「是否存在 tag」来做逻辑。

    extension CSSSelector {
        public func specificity() -> Specifity {
         
            if case CSSSelector.Simple(let simple) = self {
                // 存在 id
                let a = simple.id == nil ? 0 : 1
                
                // class 个数
                let b = simple.classes.count
                
                // 存在 tag
                let c = simple.tagName == nil ? 0 : 1
                
                return Specifity(a, b, c)
            }
            
            return Specifity(0, 0, 0)
        }
    }
    

    选择器解析

    由于我们支持分组选择器,它是一组选择器,以 , 分隔。比如:

    div.test.test2, #main {
    }
    

    这里只需重点关注单个选择器的解析,因为分组选择器解析只是循环调用单个选择器的解析方式。

    单个选择器解析

    不同选择器的区分,有些比较明显的规则:

    • * 是通配符
    • . 开头的是 class
    • # 开头的是 id

    另外,不在规则之内的,我们将做如下处理:

    • 其余情况,如果字符满足一定规则,认为是元素
    • 剩下的,认为无效

    下面,我们来一一分析。

    • 对于通配符 * 来说,不需要进行数据填充,选择器中的 id,tag,classes 全部为空就好。因为这样就能匹配任意元素。

    • 对于 . 开头的字符,属于 class。那么将 class 名称解析出来即可。

    class 名称需满足一定条件,即数组、字母、下划线、横杠的组合,比如 test-2_a。我们将其称之为有效字符串。注:下面很多地方都会用到这个判定规则。

    // 有效标识,数字、字母、_-
    func valideIdentifierChar(c: Character) -> Bool {
        if c.isNumber || c.isLetter || c == "-" || c == "_" {
            return true
        }
        
        return false
    }
    
    // 解析标识符
    mutating func parseIdentifier() -> String {
        // 字母数字-_
        return self.sourceHelper.consumeWhile(test: validIdentifierChar)
    }
    
    • 对于 # 开头的字符,属于 id 选择器。同样使用有效字符串判定规则,将 id 名称解析出来。

    • 其他情况,如果字符串是有效字符串,认为是元素。

    • 再剩下的,属于无效字符,退出解析过程。

    整个解析过程如下:

    // 解析选择器
    // tag#id.class1.class2
    mutating func parseSimpleSelector() -> SimpleSelector {
        var selector = SimpleSelector(tagName: nil, id: nil, classes: [])
        
        outerLoop: while !self.sourceHelper.eof() {
            switch self.sourceHelper.nextCharacter() {
            // id
            case "#":
                _ = self.sourceHelper.consumeCharacter()
                selector.id = self.parseIdentifier()
                break
                
            // class
            case ".":
                _ = self.sourceHelper.consumeCharacter()
                let cls = parseIdentifier()
                selector.classes.append(cls)
                break
                
            // 通配符,selector 中无需数据,可任意匹配
            case "*":
                _ = self.sourceHelper.consumeCharacter()
                break
                
            // tag
            case let c where valideIdentifierChar(c: c):
                selector.tagName = parseIdentifier()
                break
                
            case _:
                break outerLoop
            }
        }
        
        return selector
    }
    

    分组选择器解析

    分组选择器的解析,循环调用上述过程,注意退出条件。当遇到 { 时,表示属性列表的开始,即可退出了。

    另外,当得到选择器列表后,还要按照选择器优先级从高到低进行排序,为下一阶段生成样式树做准备。

    // 对 selector 进行排序,优先级从高到低
    selectors.sort { (s1, s2) -> Bool in
        s1.specificity() > s2.specificity()
    }
    

    属性解析

    属性的规则定义比较明了。它以 : 分隔属性名和属性值,以 ; 结尾。

    属性名:属性值;
    
    margin-top: 10px;
    

    照旧,先看单条属性的解析。

    • 解析出属性名,仍参照上面有效字符的规则。
    • 确保存在 : 分隔符。
    • 解析属性值。
    • 确保以 ; 结束。

    属性值解析

    由于属性值包含三种情况,稍微有点复杂。

    1. 色值解析

    色值以 # 开头,这点很好区分。接下来是 rgba 的值,8 位十六进制字符。

    不过,我们平常不会把 alpha 全都写上。因此需兼容只有 6 位的情况,此时 alpha 默认为 1。

    思路很直观,只需逐次取出两位字符,转换为十进制数即可。

    • 取出两位字符,转换为十进制。
    mutating func parseHexPair() -> UInt8 {
            // 取出 2 位字符
            let s = self.sourceHelper.consumeNCharacter(count: 2)
            
            // 转化为整数
            let value = UInt8(s, radix: 16) ?? 0
            
            return value
        }
    
    • 逐个取出 rgb。如果存在 alpha,那么进行解析。
    // 解析色值,只支持十六进制,以 # 开头, #897722
        mutating func parseColor() -> Value {
            assert(self.sourceHelper.consumeCharacter() == "#")
            
            let r = parseHexPair()
            let g = parseHexPair()
            let b = parseHexPair()
    
            var a: UInt8 = 255
            
            // 如果有 alpha
            if self.sourceHelper.nextCharacter() != ";" {
                a = parseHexPair()
            }
            
            return Value.Color(r, g, b, a)
        }
        
        
    

    2. 长度数值解析

    width: 10px;
    

    此时,属性值 = 浮点数值 + 单位。

    • 首先,解析出浮点数值。这里简单处理,「数字」和「点号」的组合,并没有严格判断有效性。
    // 解析浮点数
    mutating func parseFloat() -> Float {
        let s = self.sourceHelper.consumeWhile { (c) -> Bool in
            c.isNumber || c == "."
        }
        
        let floatValue = (s as NSString).floatValue
        return floatValue
    }
    
    • 然后,解析单位。单位只支持 px。
    // 解析单位
    mutating func parseUnit() -> Unit {
        let unit = parseIdentifier()
        if unit == "px" {
            return Unit.Px
        }
        
        assert(false, "Unexpected unit")
    }
    

    3. 关键字,也就是普通字符串

    关键字还是依据有效字符的规则,将其提取出来即可。

    属性列表解析

    当解析出单条属性后,属性列表就很简单了。同样的套路,循环。

    • 确保字符以 { 开头。
    • 当遇到 },则说明属性声明完毕。

    过程如下所示:

    // 解析声明的属性列表
    /**
     {
        margin-top: 10px;
        margin-bottom: 10px
     }
     */
    mutating func parseDeclarations() -> [Declaration] {
        var declarations: [Declaration] = []
        
        // 以 { 开头
        assert(self.sourceHelper.consumeCharacter() == "{")
        
        while true {
            self.sourceHelper.consumeWhitespace()
            
            // 如果遇到 },说明规则声明结束
            if self.sourceHelper.nextCharacter() == "}" {
                _ = self.sourceHelper.consumeCharacter()
                break
            }
            
            // 解析单条属性
            let declaration = parseDeclaration()
            declarations.append(declaration)
        }
        
        return declarations
    }
    

    规则解析

    由于单条规则由选择器列表+属性列表组成,上面已经完成了选择器和属性的解析。那么要想得到规则,只需将两者进行组合即可。

    mutating func parseRule() -> Rule {
            // 解析选择器
        let selectors = parseSelectors()
    
            // 解析属性
        let declaration = parseDeclarations()
        
        return Rule(selectors: selectors, declarations: declaration)
    }
    

    解析整个规则列表,也就是循环调用单条规则的解析。

    // 解析 css 规则
    mutating func parseRules() -> [Rule] {
        var rules:[Rule] = []
        
        // 循环解析规则
        while true {
            self.sourceHelper.consumeWhitespace()
            
            if self.sourceHelper.eof() {
                break
            }
            
                    // 解析单条规则
            let rule = parseRule()
            rules.append(rule)
        }
        
        return rules
    }
    

    生成样式表

    样式表是由规则列表组成,将上一步中解析出来的规则列表套进样式表中就可以了。

    // 对外提供的解析方法,返回样式表
    mutating public func parse(source: String) -> StyleSheet {
        self.sourceHelper.updateInput(input: source)
        
        let rules: [Rule] = parseRules()
        
        return StyleSheet(rules: rules)
    }
    

    测试代码

    let css = """
         .test {
            padding: 0px;
            margin: 10px;
            position: absolute;
         }
    
        p {
            font-size: 10px;
            color: #ff908912;
        }
    """
    
    // css 解析
    var cssParser = CSSParser()
    let styleSheet = cssParser.parse(source: css)
    print(styleSheet)
    

    可用如上代码进行测试,看看输出结果。

    完整代码可点此查看

    总结

    这一讲,我们主要介绍了如何进行单个选择器、单个属性、单条规则的解析,以及如何将它们组合起来,完成整体解析,最终生成样式表。

    这几部分的解析,思考方式上有个共同点。从整体到局部,再从局部回到整体。

    先将整体解析任务拆分为单个目标,这样问题就变小了。专注完成单个目标的解析,再循环调用单个解析,从而实现整体目标。

    下一篇将介绍样式树的生成。敬请期待~

    参考资料

    相关文章

      网友评论

          本文标题:听说你想写个渲染引擎 - css 解析

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