大家好,我是微微笑的蝸牛,??。
上篇文章中,我們講述了 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]
}
整體數據結構如下圖所示:
關于選擇器優先級,通過一個三元組來區分。
// 用于選擇器排序,優先級從高到低分別是 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)
可用如上代碼進行測試,看看輸出結果。
總結
這一講,我們主要介紹了如何進行單個選擇器、單個屬性、單條規則的解析,以及如何將它們組合起來,完成整體解析,最終生成樣式表。
這幾部分的解析,思考方式上有個共同點。從整體到局部,再從局部回到整體。
先將整體解析任務拆分為單個目標,這樣問題就變小了。專注完成單個目標的解析,再循環調用單個解析,從而實現整體目標。
下一篇將介紹樣式樹的生成。敬請期待~