sabry/sabry_intrnl/src/syntax/mod.rs
2024-12-13 10:34:23 +05:00

255 lines
6.9 KiB
Rust

use ostrta::OneSyntaxToRuleThemAll;
use raffia::{
ast::{
ClassSelector, CompoundSelector, IdSelector, NestingSelector, PseudoClassSelector,
SimpleSelector, Statement, Stylesheet, TypeSelector,
},
ParserBuilder,
};
pub mod ostrta;
/// Convenience wrapper for [Stylesheet]
pub struct StylesheetAdapter<'s> {
pub syntax: OneSyntaxToRuleThemAll,
source: &'s str,
stylesheet: Stylesheet<'s>,
}
impl<'s> StylesheetAdapter<'s> {
pub fn new(
source: &'s str,
syntax: OneSyntaxToRuleThemAll,
) -> Result<Self, raffia::error::Error> {
Ok(Self {
stylesheet: ParserBuilder::new(source)
.ignore_comments()
.syntax(syntax.into())
.build()
.parse()?,
syntax,
source,
})
}
pub fn source(&self) -> &str {
self.source
}
pub fn selectors(&self) -> Vec<CompoundSelector<'s>> {
Self::selectors_of(self.stylesheet.statements.iter().cloned()).collect()
}
pub fn class_selectors(&self) -> Vec<ClassSelector<'s>> {
self.selectors_by(|c| c.as_class().cloned())
}
pub fn id_selectors(&self) -> Vec<IdSelector<'s>> {
self.selectors_by(|c| c.as_id().cloned())
}
pub fn type_selectors(&self) -> Vec<TypeSelector<'s>> {
self.selectors_by(|c| c.as_type().cloned())
}
pub fn nesting_selectors(&self) -> Vec<NestingSelector<'s>> {
self.selectors_by(|c| c.as_nesting().cloned())
}
/// special selector case searching for :g(whatever), :glob(whatever), :global(whatever)
pub fn glob_modified_selectors(&self) -> Vec<PseudoClassSelector<'s>> {
self.selectors_by(|c| {
let cps = match c.as_pseudo_class() {
Some(s) => s.clone(),
_ => return None,
};
let ident = match cps.name.as_literal().map(|r| r.raw) {
Some(i) => i,
_ => return None,
};
if ident == "global" {
Some(cps)
} else {
None
}
})
}
/// Return collected [Vec] of owned values modulo `T`, filtered by `F`.
///
/// This is preferred way to apply custom filtering on selectors over
/// `selectors().iter().filter_map()`
/// as it does not allocate intermediate vector
pub fn selectors_by<T, F>(&self, form: F) -> Vec<T>
where
F: FnMut(SimpleSelector<'s>) -> Option<T>,
{
Self::selectors_of(self.stylesheet.statements.iter().cloned())
.flat_map(|cs| cs.children)
.filter_map(form)
.collect()
}
fn selectors_of(
c: impl Iterator<Item = Statement<'s>>,
) -> impl Iterator<Item = CompoundSelector<'s>> {
// Hello. Its Dan.
// Im not proud of this,
// but more effective implementation
// was SO unreadable and SO infected with lifetimes
// I suck at those things
// For everyone trying to make this MORE OPTIMAL:
// increment the hour-counter and post your name here:
// 4 - Danik
// 5 - Yiffy
// 8 - Danik
c.filter_map(|s| s.as_qualified_rule().cloned())
.flat_map(|q| {
let thissels = q
.selector
.selectors
.iter()
.flat_map(|sel| &sel.children)
.filter_map(|sc| sc.as_compound_selector())
.cloned();
let blockstmt = q.block.statements.iter().cloned();
Self::selectors_of(blockstmt)
.chain(thissels)
.collect::<Vec<_>>()
})
}
}
#[cfg(test)]
mod test {
use std::collections::HashSet;
use raffia::Spanned;
use crate::syntax::ostrta::OneSyntaxToRuleThemAll;
use super::StylesheetAdapter;
#[test]
fn sels() {
let source_scss = "
.r1{
.r2.r3 {
color: red;
}
&-rod {
color: red;
&[value] {
color: red;
}
}
}
div#id2 {
& > span.seled {
color: red;
& + ul#id2[attr] {
color: red;
}
& + .uled[attr] {
color: red;
}
}
&::after{
color: red;
}
}
";
let source_sass = "
.r1
.r2.r3
color: red
&-rod
color: red
&[value]
color: red
div#id2
& > span.seled
color: red
& + ul#id2[attr]
color: red
& + .uled[attr]
color: red
&::after
color: red
";
let expect_classes = HashSet::from(["r2", "r3", "r1", "uled", "seled"]);
let expect_ids = HashSet::from(["id2", "id2"]);
let expect_tags = HashSet::from(["ul", "div", "span"]);
//scss
let adp = StylesheetAdapter::new(source_scss, OneSyntaxToRuleThemAll::Scss).unwrap();
let classes = adp
.class_selectors()
.iter()
.filter_map(|c| c.name.as_literal().map(|l| l.raw))
.collect::<HashSet<_>>();
let ids = adp
.id_selectors()
.iter()
.filter_map(|i| i.name.as_literal().map(|l| l.raw))
.collect::<HashSet<_>>();
let tags = adp
.type_selectors()
.iter()
.filter_map(|t| t.as_tag_name())
.filter_map(|t| t.name.name.as_literal().map(|l| l.raw))
.collect::<HashSet<_>>();
assert_eq!(expect_classes, classes);
assert_eq!(expect_ids, ids);
assert_eq!(expect_tags, tags);
//sass
let adp = StylesheetAdapter::new(source_sass, OneSyntaxToRuleThemAll::Sass).unwrap();
let classes = adp
.class_selectors()
.iter()
.filter_map(|c| c.name.as_literal().map(|l| l.raw))
.collect::<HashSet<_>>();
let ids = adp
.id_selectors()
.iter()
.filter_map(|i| i.name.as_literal().map(|l| l.raw))
.collect::<HashSet<_>>();
let tags = adp
.type_selectors()
.iter()
.filter_map(|t| t.as_tag_name())
.filter_map(|t| t.name.name.as_literal().map(|l| l.raw))
.collect::<HashSet<_>>();
assert_eq!(expect_classes, classes);
assert_eq!(expect_ids, ids);
assert_eq!(expect_tags, tags);
}
#[test]
fn rename_class() {
let scss = ".cls1{color:red; .cls2{color:green;}}";
let newname = "cls203-jg7ihgjhftyfhjh";
let expect = ".cls1{color:red; .cls203-jg7ihgjhftyfhjh{color:green;}}";
let adp = StylesheetAdapter::new(scss, OneSyntaxToRuleThemAll::Scss).unwrap();
let classes = adp.class_selectors();
let target_class = classes[0].clone();
let span = target_class.name.span();
let renamed = format!("{}{}{}", &scss[..span.start], newname, &scss[span.end..]);
assert_eq!(renamed, expect);
}
}