423 lines
13 KiB
Rust
423 lines
13 KiB
Rust
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(),
|
|
})
|
|
}
|
|
}
|
|
|
|
pub 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);
|
|
}
|
|
}
|