-
-
Notifications
You must be signed in to change notification settings - Fork 481
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(css_semantic): build semantic model for css (#3546)
- Loading branch information
1 parent
7a8a1cc
commit e835ce5
Showing
10 changed files
with
434 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
|
||
[package] | ||
authors.workspace = true | ||
categories.workspace = true | ||
description = "Biome's semantic model for CSS" | ||
edition.workspace = true | ||
homepage.workspace = true | ||
keywords.workspace = true | ||
license.workspace = true | ||
name = "biome_css_semantic" | ||
repository.workspace = true | ||
version = "0.0.0" | ||
|
||
[dependencies] | ||
biome_css_syntax = { workspace = true } | ||
biome_rowan = { workspace = true } | ||
rustc-hash = { workspace = true } | ||
|
||
[dev-dependencies] | ||
biome_css_parser = { path = "../biome_css_parser" } | ||
|
||
[lints] | ||
workspace = true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
use std::collections::VecDeque; | ||
|
||
use biome_css_syntax::{AnyCssSelector, CssRelativeSelector, CssSyntaxKind::*}; | ||
use biome_rowan::{AstNode, TextRange}; | ||
|
||
use crate::semantic_model::model::Specificity; | ||
|
||
#[derive(Debug)] | ||
pub enum SemanticEvent { | ||
RuleStart(TextRange), | ||
RuleEnd, | ||
SelectorDeclaration { | ||
name: String, | ||
range: TextRange, | ||
specificity: Specificity, | ||
}, | ||
PropertyDeclaration { | ||
property: String, | ||
value: String, | ||
property_range: TextRange, | ||
value_range: TextRange, | ||
}, | ||
} | ||
|
||
#[derive(Default, Debug)] | ||
pub struct SemanticEventExtractor { | ||
stash: VecDeque<SemanticEvent>, | ||
current_rule_stack: Vec<TextRange>, | ||
} | ||
|
||
impl SemanticEventExtractor { | ||
pub fn enter(&mut self, node: &biome_css_syntax::CssSyntaxNode) { | ||
match node.kind() { | ||
kind if kind == CSS_QUALIFIED_RULE || kind == CSS_NESTED_QUALIFIED_RULE => { | ||
let range = node.text_range(); | ||
self.stash.push_back(SemanticEvent::RuleStart(range)); | ||
self.current_rule_stack.push(range); | ||
} | ||
CSS_SELECTOR_LIST => { | ||
node.children() | ||
.filter_map(AnyCssSelector::cast) | ||
.for_each(|s| self.process_selector(s)); | ||
} | ||
CSS_RELATIVE_SELECTOR_LIST => { | ||
node.children() | ||
.filter_map(CssRelativeSelector::cast) | ||
.filter_map(|s| s.selector().ok()) | ||
.for_each(|s| self.process_selector(s)); | ||
} | ||
CSS_DECLARATION => { | ||
if let Some(property_name) = node.first_child().and_then(|p| p.first_child()) { | ||
if let Some(value) = property_name.next_sibling() { | ||
self.stash.push_back(SemanticEvent::PropertyDeclaration { | ||
property: property_name.text_trimmed().to_string(), | ||
value: value.text_trimmed().to_string(), | ||
property_range: property_name.text_range(), | ||
value_range: value.text_range(), | ||
}); | ||
} | ||
} | ||
} | ||
_ => {} | ||
} | ||
} | ||
|
||
fn process_selector(&mut self, selector: AnyCssSelector) { | ||
match selector { | ||
AnyCssSelector::CssComplexSelector(s) => { | ||
if let Ok(l) = s.left() { | ||
self.add_selector_event(l.text(), l.range()); | ||
} | ||
if let Ok(r) = s.right() { | ||
self.add_selector_event(r.text(), r.range()); | ||
} | ||
} | ||
AnyCssSelector::CssCompoundSelector(selector) => { | ||
self.add_selector_event(selector.text().to_string(), selector.range()); | ||
} | ||
_ => {} | ||
} | ||
} | ||
|
||
fn add_selector_event(&mut self, name: String, range: TextRange) { | ||
self.stash.push_back(SemanticEvent::SelectorDeclaration { | ||
name, | ||
range, | ||
specificity: Specificity(0, 0, 0), // TODO: Implement this | ||
}); | ||
} | ||
|
||
pub fn leave(&mut self, node: &biome_css_syntax::CssSyntaxNode) { | ||
if matches!( | ||
node.kind(), | ||
biome_css_syntax::CssSyntaxKind::CSS_QUALIFIED_RULE | ||
| biome_css_syntax::CssSyntaxKind::CSS_NESTED_QUALIFIED_RULE | ||
) { | ||
self.current_rule_stack.pop(); | ||
self.stash.push_back(SemanticEvent::RuleEnd); | ||
} | ||
} | ||
|
||
pub fn pop(&mut self) -> Option<SemanticEvent> { | ||
self.stash.pop_front() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
mod events; | ||
mod semantic_model; | ||
|
||
pub use events::*; | ||
pub use semantic_model::*; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
use biome_css_syntax::{CssRoot, CssSyntaxKind, CssSyntaxNode}; | ||
use biome_rowan::TextRange; | ||
use rustc_hash::FxHashMap; | ||
|
||
use super::model::{Declaration, Rule, Selector, SemanticModel, SemanticModelData}; | ||
use crate::events::SemanticEvent; | ||
|
||
pub struct SemanticModelBuilder { | ||
root: CssRoot, | ||
node_by_range: FxHashMap<TextRange, CssSyntaxNode>, | ||
rules: Vec<Rule>, | ||
current_rule_stack: Vec<Rule>, | ||
} | ||
|
||
impl SemanticModelBuilder { | ||
pub fn new(root: CssRoot) -> Self { | ||
Self { | ||
root, | ||
node_by_range: FxHashMap::default(), | ||
rules: Vec::new(), | ||
current_rule_stack: Vec::new(), | ||
} | ||
} | ||
|
||
pub fn build(self) -> SemanticModel { | ||
let data = SemanticModelData { | ||
root: self.root, | ||
node_by_range: self.node_by_range, | ||
rules: self.rules, | ||
}; | ||
SemanticModel::new(data) | ||
} | ||
|
||
#[inline] | ||
pub fn push_node(&mut self, node: &CssSyntaxNode) { | ||
use CssSyntaxKind::*; | ||
if matches!( | ||
node.kind(), | ||
CSS_SELECTOR_LIST | CSS_DECLARATION | CSS_DECLARATION_OR_RULE_LIST | CSS_QUALIFIED_RULE | ||
) { | ||
self.node_by_range.insert(node.text_range(), node.clone()); | ||
} | ||
} | ||
|
||
#[inline] | ||
pub fn push_event(&mut self, event: SemanticEvent) { | ||
match event { | ||
SemanticEvent::RuleStart(range) => { | ||
let new_rule = Rule { | ||
selectors: Vec::new(), | ||
declarations: Vec::new(), | ||
children: Vec::new(), | ||
range, | ||
}; | ||
self.current_rule_stack.push(new_rule); | ||
} | ||
SemanticEvent::RuleEnd => { | ||
if let Some(completed_rule) = self.current_rule_stack.pop() { | ||
if let Some(parent_rule) = self.current_rule_stack.last_mut() { | ||
parent_rule.children.push(completed_rule); | ||
} else { | ||
self.rules.push(completed_rule); | ||
} | ||
} | ||
} | ||
SemanticEvent::SelectorDeclaration { | ||
name, | ||
range, | ||
specificity, | ||
} => { | ||
if let Some(current_rule) = self.current_rule_stack.last_mut() { | ||
current_rule.selectors.push(Selector { | ||
name, | ||
range, | ||
specificity, | ||
}); | ||
} | ||
} | ||
SemanticEvent::PropertyDeclaration { | ||
property, | ||
value, | ||
property_range, | ||
value_range, | ||
} => { | ||
if let Some(current_rule) = self.current_rule_stack.last_mut() { | ||
current_rule.declarations.push(Declaration { | ||
property, | ||
value, | ||
property_range, | ||
value_range, | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
mod builder; | ||
pub(crate) mod model; | ||
|
||
use biome_css_syntax::CssRoot; | ||
use biome_rowan::AstNode; | ||
use builder::SemanticModelBuilder; | ||
use model::SemanticModel; | ||
|
||
use crate::events::SemanticEventExtractor; | ||
|
||
pub fn semantic_model(root: &CssRoot) -> SemanticModel { | ||
let mut extractor = SemanticEventExtractor::default(); | ||
let mut builder = SemanticModelBuilder::new(root.clone()); | ||
|
||
let root = root.syntax(); | ||
for node in root.preorder() { | ||
match node { | ||
biome_css_syntax::WalkEvent::Enter(node) => { | ||
builder.push_node(&node); | ||
extractor.enter(&node); | ||
} | ||
biome_css_syntax::WalkEvent::Leave(node) => extractor.leave(&node), | ||
} | ||
} | ||
|
||
while let Some(e) = extractor.pop() { | ||
builder.push_event(e); | ||
} | ||
|
||
builder.build() | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use biome_css_parser::parse_css; | ||
use biome_css_parser::CssParserOptions; | ||
|
||
#[test] | ||
fn test_simple_ruleset() { | ||
let parse = parse_css( | ||
r#"p { | ||
font-family: verdana; | ||
font-size: 20px; | ||
}"#, | ||
CssParserOptions::default(), | ||
); | ||
|
||
let root = parse.tree(); | ||
let model = super::semantic_model(&root); | ||
let rule = model.rules().first().unwrap(); | ||
|
||
assert_eq!(rule.selectors.len(), 1); | ||
assert_eq!(rule.declarations.len(), 2); | ||
} | ||
#[test] | ||
fn test_nested_selector() { | ||
let parse = parse_css( | ||
r#".parent { | ||
color: blue; | ||
.child { | ||
color: red; | ||
} | ||
}"#, | ||
CssParserOptions::default(), | ||
); | ||
|
||
let root = parse.tree(); | ||
let model = super::semantic_model(&root); | ||
let rule = model.rules().first().unwrap(); | ||
|
||
assert_eq!(rule.selectors.len(), 1); | ||
assert_eq!(rule.declarations.len(), 1); | ||
assert_eq!(rule.children.len(), 1); | ||
} | ||
|
||
#[test] | ||
fn debug() { | ||
let parse = parse_css( | ||
r#"[a="b"i], [ a="b"i], [ a ="b"i], [ a = "b"i], [ a = "b" i], [ a = "b" i ] {}"#, | ||
CssParserOptions::default(), | ||
); | ||
|
||
let root = parse.tree(); | ||
let model = super::semantic_model(&root); | ||
dbg!(&model.rules()); | ||
} | ||
} |
Oops, something went wrong.