灵符lingfu
H

horizon-browser

↓ 0 downloads

Horizon: a real box-model HTML/CSS layout engine with a Canvas 2D surface, no_std + alloc, zero dependencies -- LingOS's next-generation browser engine, sibling to bring-browser

Keywords browser, html, css, layout, canvas, no_std, lingos
lingfu install horizon-browser
LicenseMIT
AuthorsLing Lin <taellinglin@gmail.com>
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (9)

src/css.rs
//! CSS subset: inline `style=""` plus `<style>` blocks, matched with tag,
//! `.class`, and `#id` selectors (no combinators, no compound selectors --
//! same staged scope `bring-browser` documents for its own CSS engine).
//! Cascade order, low to high priority: tag/universal rules, class rules,
//! id rules, inline `style=""` -- each pass applied in source order so a
//! later same-specificity rule overrides an earlier one, which is a
//! reasonable approximation of real CSS specificity without computing it.

use alloc::string::{String, ToString};
use alloc::vec::Vec;

use crate::dom::{Dom, Tag};

#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Display {
    Block,
    Inline,
    None,
}

#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Align {
    Left,
    Center,
    Right,
}

#[derive(Clone, Copy, Debug)]
pub struct Style {
    pub display: Display,
    pub color: Option<u32>,
    pub background: Option<u32>,
    pub width: Option<u32>,
    pub height: Option<u32>,
    pub margin: u32,
    pub padding: u32,
    pub bold: bool,
    pub align: Align,
}

impl Style {
    pub fn default_for(tag: Tag) -> Style {
        let display = match tag {
            Tag::Span | Tag::A | Tag::B | Tag::I | Tag::Text | Tag::Br => Display::Inline,
            Tag::Script | Tag::Style | Tag::Title | Tag::Head => Display::None,
            _ => Display::Block,
        };
        let bold = matches!(tag, Tag::B | Tag::H1 | Tag::H2 | Tag::H3);
        let margin = match tag {
            Tag::P | Tag::Div | Tag::Ul | Tag::Ol | Tag::H1 | Tag::H2 | Tag::H3 | Tag::H4 | Tag::H5 | Tag::H6 | Tag::Pre | Tag::Canvas => 6,
            _ => 0,
        };
        Style { display, color: None, background: None, width: None, height: None, margin, padding: 0, bold, align: Align::Left }
    }
}

enum Selector {
    Tag(Tag),
    Class(String),
    Id(String),
    Universal,
}

struct Rule {
    selector: Selector,
    decls: Vec<(String, String)>,
}

/// Collect every `<style>` block's text (source order) into one sheet, parse
/// it, and cascade it plus each element's inline `style=""` onto a
/// `Style` per DOM node, indexed the same as `dom.nodes`.
pub fn cascade(dom: &Dom) -> Vec<Style> {
    let mut styles: Vec<Style> = dom.nodes.iter().map(|n| Style::default_for(n.tag)).collect();

    let mut sheet = String::new();
    collect_style_text(dom, dom.root, &mut sheet);
    let rules = parse_stylesheet(&sheet);

    for pass in 0..3u8 {
        for rule in &rules {
            let matches_pass = match (&rule.selector, pass) {
                (Selector::Tag(_), 0) | (Selector::Universal, 0) => true,
                (Selector::Class(_), 1) => true,
                (Selector::Id(_), 2) => true,
                _ => false,
            };
            if !matches_pass {
                continue;
            }
            for (idx, node) in dom.nodes.iter().enumerate() {
                if selector_matches(&rule.selector, node.tag, node.attr("class"), node.attr("id")) {
                    for (k, v) in &rule.decls {
                        apply_decl(&mut styles[idx], k, v);
                    }
                }
            }
        }
    }

    // Inline style="" always wins last.
    for (idx, node) in dom.nodes.iter().enumerate() {
        if let Some(inline) = node.attr("style") {
            for (k, v) in parse_decls(inline) {
                apply_decl(&mut styles[idx], &k, &v);
            }
        }
    }

    styles
}

fn collect_style_text(dom: &Dom, idx: usize, out: &mut String) {
    let node = &dom.nodes[idx];
    if node.tag == Tag::Style {
        out.push_str(&node.text);
        out.push('\n');
    }
    for &c in &node.children {
        collect_style_text(dom, c, out);
    }
}

fn selector_matches(sel: &Selector, tag: Tag, class: Option<&str>, id: Option<&str>) -> bool {
    match sel {
        Selector::Universal => true,
        Selector::Tag(t) => *t == tag,
        Selector::Class(c) => class.map(|cl| cl.split_ascii_whitespace().any(|w| w == c)).unwrap_or(false),
        Selector::Id(i) => id.map(|v| v == i).unwrap_or(false),
    }
}

fn parse_stylesheet(css: &str) -> Vec<Rule> {
    let mut rules = Vec::new();
    let bytes = css.as_bytes();
    let n = bytes.len();
    let mut i = 0usize;
    while i < n {
        while i < n && bytes[i].is_ascii_whitespace() {
            i += 1;
        }
        if i >= n {
            break;
        }
        if bytes[i..].starts_with(b"/*") {
            i = match find(&bytes[i + 2..], b"*/") {
                Some(end) => i + 2 + end + 2,
                None => n,
            };
            continue;
        }
        let sel_start = i;
        while i < n && bytes[i] != b'{' {
            i += 1;
        }
        if i >= n {
            break;
        }
        let sel_str = core::str::from_utf8(&bytes[sel_start..i]).unwrap_or("").trim();
        i += 1;
        let decl_start = i;
        while i < n && bytes[i] != b'}' {
            i += 1;
        }
        let decl_str = core::str::from_utf8(&bytes[decl_start..i.min(n)]).unwrap_or("");
        if i < n {
            i += 1;
        }
        let decls = parse_decls(decl_str);
        if decls.is_empty() {
            continue;
        }
        for token in sel_str.split(',') {
            if let Some(sel) = parse_selector(token) {
                rules.push(Rule { selector: sel, decls: decls.clone() });
            }
        }
    }
    rules
}

fn parse_selector(s: &str) -> Option<Selector> {
    let s = s.trim();
    if s.is_empty() {
        return None;
    }
    if let Some(rest) = s.strip_prefix('.') {
        return Some(Selector::Class(rest.to_string()));
    }
    if let Some(rest) = s.strip_prefix('#') {
        return Some(Selector::Id(rest.to_string()));
    }
    if s == "*" {
        return Some(Selector::Universal);
    }
    Some(Selector::Tag(Tag::from_name(s.as_bytes())))
}

fn parse_decls(s: &str) -> Vec<(String, String)> {
    s.split(';')
        .filter_map(|d| {
            let d = d.trim();
            if d.is_empty() {
                return None;
            }
            let (k, v) = d.split_once(':')?;
            Some((k.trim().to_ascii_lowercase(), v.trim().to_string()))
        })
        .collect()
}

fn apply_decl(style: &mut Style, key: &str, value: &str) {
    match key {
        "display" => {
            style.display = match value {
                "none" => Display::None,
                "inline" => Display::Inline,
                "block" => Display::Block,
                _ => style.display,
            };
        },
        "color" => {
            if let Some(c) = parse_color(value) {
                style.color = Some(c);
            }
        },
        "background-color" | "background" => {
            if let Some(c) = parse_color(value) {
                style.background = Some(c);
            }
        },
        "width" => {
            if let Some(px) = parse_px(value) {
                style.width = Some(px);
            }
        },
        "height" => {
            if let Some(px) = parse_px(value) {
                style.height = Some(px);
            }
        },
        "margin" => {
            if let Some(px) = parse_px(value) {
                style.margin = px;
            }
        },
        "padding" => {
            if let Some(px) = parse_px(value) {
                style.padding = px;
            }
        },
        "font-weight" => {
            style.bold = value.eq_ignore_ascii_case("bold") || value == "700" || value == "600" || value == "800" || value == "900";
        },
        "text-align" => {
            style.align = match value {
                "center" => Align::Center,
                "right" => Align::Right,
                _ => Align::Left,
            };
        },
        _ => {},
    }
}

fn parse_px(v: &str) -> Option<u32> {
    let v = v.trim();
    let digits: String = v.chars().take_while(|c| c.is_ascii_digit()).collect();
    if digits.is_empty() {
        return None;
    }
    digits.parse::<u32>().ok()
}

/// `#rgb`, `#rrggbb`, `rgb(r,g,b)`, or a small named-color subset. Returns
/// `0x00RRGGBB`. Mirrors `bring-browser`'s color grammar exactly.
fn parse_color(s: &str) -> Option<u32> {
    let s = s.trim();
    if s.is_empty() {
        return None;
    }
    let b = s.as_bytes();
    let hexval = |c: u8| -> Option<u32> {
        match c {
            b'0'..=b'9' => Some((c - b'0') as u32),
            b'a'..=b'f' => Some((c - b'a' + 10) as u32),
            b'A'..=b'F' => Some((c - b'A' + 10) as u32),
            _ => None,
        }
    };
    if b[0] == b'#' {
        let h = &b[1..];
        if h.len() == 3 {
            let r = hexval(h[0])?;
            let g = hexval(h[1])?;
            let bl = hexval(h[2])?;
            return Some((r * 17) << 16 | (g * 17) << 8 | (bl * 17));
        } else if h.len() == 6 {
            let r = hexval(h[0])? * 16 + hexval(h[1])?;
            let g = hexval(h[2])? * 16 + hexval(h[3])?;
            let bl = hexval(h[4])? * 16 + hexval(h[5])?;
            return Some(r << 16 | g << 8 | bl);
        }
        return None;
    }
    if s.len() >= 4 && s[..4].eq_ignore_ascii_case("rgb(") {
        let inner = &b[4..b.len().saturating_sub(1)];
        let mut vals = [0u32; 3];
        let mut vi = 0usize;
        let mut cur = 0u32;
        let mut any = false;
        for &c in inner {
            match c {
                b'0'..=b'9' => {
                    cur = cur * 10 + (c - b'0') as u32;
                    any = true;
                },
                b',' => {
                    if vi < 3 {
                        vals[vi] = cur.min(255);
                        vi += 1;
                    }
                    cur = 0;
                    any = false;
                },
                _ => {},
            }
        }
        if any && vi < 3 {
            vals[vi] = cur.min(255);
            vi += 1;
        }
        if vi == 3 {
            return Some(vals[0] << 16 | vals[1] << 8 | vals[2]);
        }
        return None;
    }
    let named: [(&str, u32); 12] = [
        ("black", 0x000000),
        ("white", 0xFFFFFF),
        ("red", 0xFF0000),
        ("green", 0x008000),
        ("blue", 0x0000FF),
        ("gray", 0x808080),
        ("grey", 0x808080),
        ("silver", 0xC0C0C0),
        ("yellow", 0xFFFF00),
        ("orange", 0xFFA500),
        ("purple", 0x800080),
        ("navy", 0x000080),
    ];
    for (name, v) in named {
        if s.eq_ignore_ascii_case(name) {
            return Some(v);
        }
    }
    None
}

fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
    if needle.is_empty() || haystack.len() < needle.len() {
        return None;
    }
    (0..=haystack.len() - needle.len()).find(|&i| &haystack[i..i + needle.len()] == needle)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::dom;

    #[test]
    fn inline_style_wins_over_stylesheet() {
        let dom = dom::parse(b"<style>p{color:red}</style><p style=\"color:#00ff00\">x</p>");
        let styles = cascade(&dom);
        let p = dom.nodes[dom.root].children[1];
        assert_eq!(styles[p].color, Some(0x00ff00));
    }

    #[test]
    fn class_selector_beats_tag_selector() {
        let dom = dom::parse(b"<style>p{color:red} .hi{color:blue}</style><p class=\"hi\">x</p>");
        let styles = cascade(&dom);
        let p = dom.nodes[dom.root].children[1];
        assert_eq!(styles[p].color, Some(0x0000FF));
    }

    #[test]
    fn id_selector_beats_class_selector() {
        let dom = dom::parse(b"<style>.hi{color:blue} #x{color:#fff}</style><p class=\"hi\" id=\"x\">x</p>");
        let styles = cascade(&dom);
        let p = dom.nodes[dom.root].children[1];
        assert_eq!(styles[p].color, Some(0xFFFFFF));
    }

    #[test]
    fn display_none_is_parsed() {
        let dom = dom::parse(b"<style>.hidden{display:none}</style><div class=\"hidden\">x</div>");
        let styles = cascade(&dom);
        let div = dom.nodes[dom.root].children[1];
        assert_eq!(styles[div].display, Display::None);
    }

    #[test]
    fn hex_and_named_colors_agree() {
        assert_eq!(parse_color("red"), parse_color("#ff0000"));
        assert_eq!(parse_color("#0f0"), Some(0x00FF00));
        assert_eq!(parse_color("rgb(1,2,3)"), Some(0x010203));
    }
}

← Back to index