initial os release. I really expect SO much bugs, guys. good luck =)
This commit is contained in:
commit
d47557dc59
1 changed files with 7965 additions and 0 deletions
423
sabry_intrnl/src/scoper/mod.rs
Normal file
423
sabry_intrnl/src/scoper/mod.rs
Normal file
|
|
@ -0,0 +1,423 @@
|
|||
use hash::ScopeHash;
|
||||
use raffia::Spanned;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::{config::SabryHashConfig, syntax::StylesheetAdapter};
|
||||
|
||||
pub mod hash;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ScopeError {
|
||||
#[error("Raffia reports parse error")]
|
||||
Raffia(raffia::error::ErrorKind),
|
||||
}
|
||||
|
||||
impl From<raffia::error::Error> for ScopeError {
|
||||
fn from(value: raffia::error::Error) -> Self {
|
||||
Self::Raffia(value.kind)
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct which represents arbitrary style scope with known style syntax and name identifier
|
||||
pub struct ArbitraryScope<'s> {
|
||||
/// Ident name for the current scope
|
||||
pub name: syn::Ident,
|
||||
adapter: StylesheetAdapter<'s>,
|
||||
}
|
||||
|
||||
impl<'s> ArbitraryScope<'s> {
|
||||
/// Parse given source code with given syntax and assign given name
|
||||
pub fn from_source(
|
||||
syntax: raffia::Syntax,
|
||||
name: syn::Ident,
|
||||
source: &'s str,
|
||||
) -> Result<Self, ScopeError> {
|
||||
Ok(Self {
|
||||
adapter: StylesheetAdapter::new(source, syntax)?,
|
||||
name,
|
||||
})
|
||||
}
|
||||
|
||||
/// Consume arbitrary scope and create a [HashedScope] in its basis
|
||||
///
|
||||
/// This function calls [HashedScope::new] under the hood
|
||||
pub fn hashed(self, config: &SabryHashConfig) -> Result<HashedScope<'s>, ScopeError> {
|
||||
let hash = ScopeHash::new(&self, config);
|
||||
HashedScope::new(hash, self)
|
||||
}
|
||||
|
||||
/// Borrow the underlying [StylesheetAdapter]
|
||||
pub fn adapter(&self) -> &StylesheetAdapter<'s> {
|
||||
&self.adapter
|
||||
}
|
||||
}
|
||||
|
||||
/// Hashed scope which holds hashed style code,
|
||||
/// vector of hashed selectors and the calculated hash itself.
|
||||
///
|
||||
/// Construction with [HashedScope::new] consumes [ArbitraryScope] and does all the job
|
||||
pub struct HashedScope<'s> {
|
||||
/// Original arbitrary scope
|
||||
pub original_scope: ArbitraryScope<'s>,
|
||||
/// Hashed style code
|
||||
pub hashed_code: String,
|
||||
/// Collection of hashed selectors
|
||||
pub hashed_selectors: Vec<HashedSelector>,
|
||||
|
||||
/// Given hash to the scope
|
||||
pub hash: ScopeHash,
|
||||
}
|
||||
|
||||
impl<'s> HashedScope<'s> {
|
||||
/// Consume [ArbitraryScope],
|
||||
/// calculate the hash,
|
||||
/// hash all the supported selectors
|
||||
/// and construct hashed source code
|
||||
///
|
||||
/// Heavy operation.
|
||||
pub fn new(hash: ScopeHash, scope: ArbitraryScope<'s>) -> Result<Self, ScopeError> {
|
||||
let origin_code = scope.adapter().source();
|
||||
let mut hashed_code = String::with_capacity(scope.adapter().source().len());
|
||||
|
||||
// get classes
|
||||
let classes = scope.adapter().class_selectors();
|
||||
let classes = classes.iter().map(|c| HashedSelector::from_class(&hash, c));
|
||||
|
||||
// get ids
|
||||
let ids = scope.adapter().id_selectors();
|
||||
let ids = ids.iter().map(|c| HashedSelector::from_id(&hash, c));
|
||||
|
||||
// get tags
|
||||
let tags = scope.adapter().type_selectors();
|
||||
let tags = tags
|
||||
.iter()
|
||||
.filter_map(|t| t.as_tag_name())
|
||||
.map(|c| HashedSelector::from_tag(&hash, c));
|
||||
|
||||
let mut hashed_selectors = classes.chain(ids).chain(tags).collect::<Vec<_>>();
|
||||
|
||||
// sorting by span start is important because of how hashed code construction works
|
||||
hashed_selectors.sort_by(|a, b| {
|
||||
let astart = a.sel.as_arbitrary().span.start;
|
||||
let bstart = b.sel.as_arbitrary().span.start;
|
||||
|
||||
astart.cmp(&bstart)
|
||||
});
|
||||
|
||||
let mut last_term_span: usize = 0;
|
||||
for sel in &hashed_selectors {
|
||||
let span = sel.sel.as_arbitrary().span.clone();
|
||||
hashed_code.push_str(&origin_code[last_term_span..span.start]);
|
||||
hashed_code.push_str(&sel.css_ident);
|
||||
last_term_span = span.end;
|
||||
}
|
||||
hashed_code.push_str(&origin_code[last_term_span..]);
|
||||
|
||||
Ok(Self {
|
||||
original_scope: scope,
|
||||
hashed_code,
|
||||
hashed_selectors,
|
||||
hash,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct which represents hashed selector inside of [HashedScope].
|
||||
///
|
||||
/// Does not hold information about hash itself, as meant to be inside of scope.
|
||||
pub struct HashedSelector {
|
||||
/// Basic unrelated to hash info about this selector
|
||||
pub sel: ScopedSelector,
|
||||
/// CSS-ish identifier for this selector, which matches what's in the hashed style code
|
||||
pub css_ident: String,
|
||||
/// HTML-ish identifier for this selector, which will match [Self::css_ident] in browser
|
||||
pub html_ident: Option<String>,
|
||||
}
|
||||
|
||||
impl HashedSelector {
|
||||
/// Construct the [HashedSelector], with given hash and parsed class selector
|
||||
pub fn from_class(hash: &ScopeHash, sel: &raffia::ast::ClassSelector) -> Self {
|
||||
let sel = ScopedSelector::from_class(sel);
|
||||
let css_ident = Self::make_hashed_css(&sel, hash);
|
||||
let html_ident = Self::make_hashed_html(&sel, hash);
|
||||
|
||||
Self {
|
||||
sel,
|
||||
css_ident,
|
||||
html_ident,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct the [HashedSelector], with given hash and parsed id selector
|
||||
pub fn from_id(hash: &ScopeHash, sel: &raffia::ast::IdSelector) -> Self {
|
||||
let sel = ScopedSelector::from_id(sel);
|
||||
let css_ident = Self::make_hashed_css(&sel, hash);
|
||||
let html_ident = Self::make_hashed_html(&sel, hash);
|
||||
|
||||
Self {
|
||||
sel,
|
||||
css_ident,
|
||||
html_ident,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct the [HashedSelector], with given hash and parsed tagname selector
|
||||
pub fn from_tag(hash: &ScopeHash, sel: &raffia::ast::TagNameSelector) -> Self {
|
||||
let sel = ScopedSelector::from_tag(sel);
|
||||
let css_ident = Self::make_hashed_css(&sel, hash);
|
||||
let html_ident = Self::make_hashed_html(&sel, hash);
|
||||
|
||||
Self {
|
||||
sel,
|
||||
css_ident,
|
||||
html_ident,
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform this selector into hashed version for CSS-ish language
|
||||
///
|
||||
/// Cooperates with the [HashedSelector::make_hashed_html], so
|
||||
/// whats this function returns will be usable in HTML with to_hashed_html_def()
|
||||
pub fn make_hashed_css(value: &ScopedSelector, hash: &ScopeHash) -> String {
|
||||
match value {
|
||||
// Class scoping is done with class composition
|
||||
ScopedSelector::Class(a) => {
|
||||
format!("{}.{}", hash.as_str(), a.ident)
|
||||
}
|
||||
// As we dont want to use two HTML props for a single ID,
|
||||
// ID scoping is done with id modification
|
||||
ScopedSelector::Id(a) => {
|
||||
format!("{}-{}", hash.as_str(), a.ident)
|
||||
}
|
||||
ScopedSelector::Tag(a) => {
|
||||
format!(".{} {}", hash.as_str(), a.ident)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform this selector into hashed version valid for HTML-ish insertion
|
||||
///
|
||||
/// Cooperates with the [HashedSelector::make_hashed_css], so
|
||||
/// whats this function returns will be usable in CSS with to_hashed_code()
|
||||
///
|
||||
/// Not every hashed selector is presentable for HTML-ish use: like `div`, in that case
|
||||
/// returns [None]
|
||||
pub fn make_hashed_html(value: &ScopedSelector, hash: &ScopeHash) -> Option<String> {
|
||||
match value {
|
||||
// Class scoping is done with class composition
|
||||
ScopedSelector::Class(a) => Some(format!("{} {}", hash.as_str(), a.ident)),
|
||||
// As we dont want to use two HTML props for a single ID,
|
||||
// ID scoping is done with id modification
|
||||
ScopedSelector::Id(a) => Some(format!("{}-{}", hash.as_str(), a.ident)),
|
||||
ScopedSelector::Tag(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Just any unary CSS-ish selector
|
||||
pub struct ArbitrarySelector {
|
||||
/// CSS-ish identifier
|
||||
pub ident: String,
|
||||
/// Span in original CSS code where it comes from
|
||||
pub span: raffia::Span,
|
||||
}
|
||||
|
||||
/// Any unary CSS-ish selector, that's able to be scoped, and then hashed
|
||||
pub enum ScopedSelector {
|
||||
Class(ArbitrarySelector),
|
||||
Id(ArbitrarySelector),
|
||||
Tag(ArbitrarySelector),
|
||||
}
|
||||
|
||||
impl ScopedSelector {
|
||||
/// Borrow the underlying [ArbitrarySelector]
|
||||
pub fn as_arbitrary(&self) -> &ArbitrarySelector {
|
||||
match self {
|
||||
Self::Class(a) => a,
|
||||
Self::Id(a) => a,
|
||||
Self::Tag(a) => a,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate rusty member ident based on selector type and CSS ident
|
||||
pub fn gen_rusty_ident(&self) -> syn::Ident {
|
||||
let basic = apply_basic_rusty_member_gen_rules(&self.as_arbitrary().ident);
|
||||
|
||||
let ready = match self {
|
||||
Self::Class(_) => basic,
|
||||
Self::Id(_) => {
|
||||
format!("the{basic}")
|
||||
}
|
||||
Self::Tag(_) => {
|
||||
format!("any{basic}")
|
||||
}
|
||||
};
|
||||
|
||||
syn::parse_str::<syn::Ident>(&ready).expect("BUG: formed ident is not Rust Ident")
|
||||
}
|
||||
|
||||
/// Construct this from class selector
|
||||
pub fn from_class(s: &raffia::ast::ClassSelector) -> Self {
|
||||
let lit = s
|
||||
.name
|
||||
.as_literal()
|
||||
.expect("BUG: class selector is not a literal");
|
||||
Self::Class(ArbitrarySelector {
|
||||
ident: lit.raw.to_string(),
|
||||
span: lit.span().clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Construct this from id selector
|
||||
pub fn from_id(s: &raffia::ast::IdSelector) -> Self {
|
||||
let lit = s
|
||||
.name
|
||||
.as_literal()
|
||||
.expect("BUG: id selector is not a literal");
|
||||
Self::Id(ArbitrarySelector {
|
||||
ident: lit.raw.to_string(),
|
||||
span: lit.span().clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Construct this from tagname selector
|
||||
pub fn from_tag(s: &raffia::ast::TagNameSelector) -> Self {
|
||||
let lit = s
|
||||
.name
|
||||
.name
|
||||
.as_literal()
|
||||
.expect("BUG: tag selector is not a literal");
|
||||
Self::Tag(ArbitrarySelector {
|
||||
ident: lit.raw.to_string(),
|
||||
span: lit.span().clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_basic_rusty_member_gen_rules(source: &str) -> String {
|
||||
let omit_regex = Regex::new(r"(^\-)|(\-$)|[^a-zA-Z0-9\-\_]")
|
||||
.expect("BUG: can not build omition regex for rusty member generation");
|
||||
|
||||
// omit forbidden chars
|
||||
let cleaned = omit_regex.replace_all(source, "").to_string();
|
||||
|
||||
// other not-so-obvious rules
|
||||
let mut target = String::with_capacity(cleaned.len());
|
||||
let mut next_uppercase: bool = false;
|
||||
for (i, c) in cleaned.chars().enumerate() {
|
||||
// first gidit is prepended with 'n'
|
||||
if i == 0 && c.is_numeric() {
|
||||
target.push('n');
|
||||
target.push(c);
|
||||
continue;
|
||||
}
|
||||
// dash is omitted and next is uppercase if may be
|
||||
if c == '-' {
|
||||
next_uppercase = true;
|
||||
continue;
|
||||
}
|
||||
target.push(if next_uppercase {
|
||||
next_uppercase = false;
|
||||
c.to_ascii_uppercase()
|
||||
} else {
|
||||
c
|
||||
});
|
||||
}
|
||||
target
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use syn::Ident;
|
||||
|
||||
use crate::{
|
||||
config::SabryHashConfig,
|
||||
scoper::{hash::ScopeHash, HashedScope},
|
||||
};
|
||||
|
||||
use super::ArbitraryScope;
|
||||
|
||||
#[test]
|
||||
fn scope_hash_codegen() {
|
||||
let code =
|
||||
".cls1{color:red; &-dark{color: black;} #id1 {color:green;} div {color:blue;}} .cls3#id2{color: black;}";
|
||||
let hash = ScopeHash::test_init("F2kf8nMs".into());
|
||||
|
||||
let expect_code = ".F2kf8nMs.cls1{color:red; &-dark{color: black;} #F2kf8nMs-id1 {color:green;} .F2kf8nMs div {color:blue;}} .F2kf8nMs.cls3#F2kf8nMs-id2{color: black;}";
|
||||
let expect_selector_htmls = HashSet::from([
|
||||
"F2kf8nMs cls1".to_string(),
|
||||
"F2kf8nMs-id1".to_string(),
|
||||
"F2kf8nMs cls3".to_string(),
|
||||
"F2kf8nMs-id2".to_string(),
|
||||
"".to_string(), // comes from `div` which has no html hashed code, still want to check presense
|
||||
]);
|
||||
|
||||
let scope = ArbitraryScope::from_source(
|
||||
raffia::Syntax::Scss,
|
||||
syn::parse_str("scope1").unwrap(),
|
||||
code,
|
||||
)
|
||||
.unwrap();
|
||||
let scope = HashedScope::new(hash, scope).unwrap();
|
||||
let scope_selector_htmls = scope
|
||||
.hashed_selectors
|
||||
.iter()
|
||||
.map(|hs| hs.html_ident.clone().unwrap_or_default())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
// check that original is untouched
|
||||
assert_eq!(code, scope.original_scope.adapter().source());
|
||||
// check that hashed matches what we expect
|
||||
assert_eq!(expect_code, scope.hashed_code);
|
||||
// check that HTML selector hashed code matches what we expect
|
||||
assert_eq!(expect_selector_htmls, scope_selector_htmls);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rusty_idents() {
|
||||
let code = "
|
||||
.cls-1{
|
||||
color:red;
|
||||
&-dark{
|
||||
color: black;
|
||||
}
|
||||
#id-1 {
|
||||
color:green;
|
||||
}
|
||||
.-txt-of {
|
||||
color:blue;
|
||||
}
|
||||
}
|
||||
.-c_ls3#4id{
|
||||
color: black;
|
||||
}";
|
||||
let expect_ident_names = HashSet::from([
|
||||
"cls1".to_string(),
|
||||
"theid1".to_string(),
|
||||
"c_ls3".to_string(),
|
||||
"then4id".to_string(),
|
||||
"txtOf".to_string(),
|
||||
]);
|
||||
|
||||
let hash_cfg = SabryHashConfig::default();
|
||||
let scope = ArbitraryScope::from_source(
|
||||
raffia::Syntax::Scss,
|
||||
syn::parse_str::<Ident>("scope2oiej").unwrap(),
|
||||
code,
|
||||
)
|
||||
.unwrap()
|
||||
.hashed(&hash_cfg)
|
||||
.unwrap();
|
||||
|
||||
let sels = scope
|
||||
.hashed_selectors
|
||||
.iter()
|
||||
.map(|s| s.sel.gen_rusty_ident())
|
||||
.map(|i| i.to_string())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
assert_eq!(expect_ident_names, sels);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue