iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 8
1

明天一樣討論 robinson 這個「玩具」專案。讓我們來直接看 robinson/src/style.rs 這邊如何實作 style。如果你還沒看過 CSS 系列的前兩部分,建議你先看一下。如果是系列的新讀者,建議你從第一篇開始讀。

這邊要注意一下,我們昨天提到的四個特性,在這邊都沒有實作,這邊做的部分只是最簡單的,把 CSS 綁定 DOM。也就是讓 DOM 能有顏色、大小之類的樣子。

程式

這邊程式碼不上色( ITHelp 不支援 Rust ORZ)

這邊我們定義一個 StyledNode 作為 style tree 的節點。還記得 DOM tree + CSS tree = style tree 嗎?

pub fn style_tree<'a>(root: &'a Node, stylesheet: &'a Stylesheet) 
-> StyledNode<'a> {

    StyledNode {
        node: root,
        specified_values: match root.node_type {
            NodeType::Element(ref elem) => 
                specified_values(elem, stylesheet),
            NodeType::Text(_) => HashMap::new()
        },
        children: root.children
                      .iter()
                      .map(|child| style_tree(child, stylesheet))
                      .collect(),
    }
    
}

每個節點對應的 DOM 和 style 的屬性

/// A node with associated style data.

pub struct StyledNode<'a> {
    pub node: &'a Node,
    pub specified_values: PropertyMap,
    pub children: Vec<StyledNode<'a>>,
}

接著從 CSS tree 中,遍佈的尋找對應的 DOM,為什麼從 CSS tree 而不是 DOM tree 呢?因為 CSS 一定會有對應的 DOM (除非是無效 CSS),而 DOM 不一定會有對應的 CSS,所以從 CSS 找 DOM 效率比較高。

還記得我們 CSS 模組做過的事情嗎? Stylesheet 裡面有很多 rules,每個 rules 裡面又有很多 selectors,對應各自的值。

所以這邊直接遍佈 Stylesheet,這時候每檢查一個 rule 會呼叫 match_rule,每次 match_rule 會再檢查 selectors。

// Find all CSS rules that match the given element.

fn matching_rules<'a>(elem: &ElementData, stylesheet: &'a Stylesheet) -> Vec<MatchedRule<'a>> {
    stylesheet.rules.iter()
              .filter_map(|rule| match_rule(elem, rule))
              .collect()
}
// If `rule` matches `elem`, return a `MatchedRule`. 
// Otherwise return `None`.

fn match_rule<'a>(elem: &ElementData, rule: &'a Rule) 
-> Option<MatchedRule<'a>> {
    rule.selectors.iter()
        .find(|selector| matches(elem, *selector))
        .map(|selector| (selector.specificity(), rule))
}

藉由之前在 DOM 模組做的,這邊可以輕鬆取得 DOM 的 CSS 是什麼。

impl ElementData {
    pub fn id(&self) -> Option<&String> {
        self.attributes.get("id")
    }

    pub fn classes(&self) -> HashSet<&str> {
        match self.attributes.get("class") {
            Some(classlist) => classlist.split(' ').collect(),
            None => HashSet::new()
        }
    }
}

再藉由 matches 由 selector 去對應 DOM 的 CSS,如果有匹配就回傳

/// Selector matching:
fn matches(elem: &ElementData, selector: &Selector) -> bool {
    match *selector {
        Selector::Simple(ref simple_selector) => 
            matches_simple_selector(elem, simple_selector)
    }
}

fn matches_simple_selector(elem: &ElementData, selector: &SimpleSelector) -> bool {
    // Check type selector
    if selector.tag_name.iter().any(|name| elem.tag_name != *name) {
        return false
    }

    // Check ID selector
    if selector.id.iter().any(|id| elem.id() != Some(id)) {
        return false;
    }

    // Check class selectors
    let elem_classes = elem.classes();
    if selector.class.iter().any(|class| !elem_classes.contains(&**class)) {
        return false;
    }

    // We didn't find any non-matching selector components.
    true
}

最後我們就讓本文一開始提到的 style_tree 藉由執行 specified_values 取得樹裡面的數值,取得方式是靠上面提到的 matching_rules

/// Apply styles to a single element, returning the specified styles.
fn specified_values(elem: &ElementData, stylesheet: &Stylesheet) -> PropertyMap {
    let mut values = HashMap::new();
    let mut rules = matching_rules(elem, stylesheet);

    // Go through the rules from lowest to highest specificity.
    rules.sort_by(|&(a, _), &(b, _)| a.cmp(&b));
    for (_, rule) in rules {
        for declaration in &rule.declarations {
            values.insert(declaration.name.clone(), declaration.value.clone());
        }
    }
    values
}

以上就是簡單的 style tree 實作方式,其實眼尖的話就可以發現沒什麼技術,等於就是好幾個迴圈去做匹配,只是寫法比較好看而已。這種寫法是完全線性,非常沒效率,最新的 Servo 的 Stylo 就針對這部份做高效能的優化,其中一項就是平行化處理樹。

希望有幫到大家,大家明天見!


關於作者

劉安齊

軟體工程師,熱愛寫程式,更喜歡推廣程式讓更多人學會


上一篇
瀏覽器引擎處理 CSS 的簡易版(二)
下一篇
Mozilla / Servo 瀏覽器引擎開發環境架設
系列文
來做個網路瀏覽器吧!Let's build a web browser!35

尚未有邦友留言

立即登入留言