initial os release. I really expect SO much bugs, guys. good luck =)
This commit is contained in:
commit
d47557dc59
7 changed files with 7965 additions and 0 deletions
120
sabry_intrnl/src/compiler/mod.rs
Normal file
120
sabry_intrnl/src/compiler/mod.rs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
use lightningcss::{printer::PrinterOptions, stylesheet::MinifyOptions, targets::Targets};
|
||||
|
||||
use crate::config::SabryConfig;
|
||||
|
||||
/// Convenience wrapper on Grass and Lightningcss
|
||||
pub struct CompilerAdapter {
|
||||
config: SabryConfig,
|
||||
}
|
||||
|
||||
impl CompilerAdapter {
|
||||
pub fn new(config: SabryConfig) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
pub fn compile_module(
|
||||
&self,
|
||||
syntax: CompilerSyntax,
|
||||
code: &str,
|
||||
) -> Result<String, SabryCompilerError> {
|
||||
let options = grass::Options::from(&self.config).input_syntax(syntax.into());
|
||||
|
||||
let mut css = grass::from_string(code, &options)?;
|
||||
|
||||
if self.config.css.minify {
|
||||
//its lightningcss time!
|
||||
let mut lightsheet = match lightningcss::stylesheet::StyleSheet::parse(
|
||||
&css,
|
||||
lightningcss::stylesheet::ParserOptions::default(),
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err(SabryCompilerError::Lightningcss()),
|
||||
};
|
||||
|
||||
let targets = Targets {
|
||||
browsers: Some(self.config.lightningcss.targets.clone().into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let printer_options = PrinterOptions {
|
||||
minify: self.config.css.minify,
|
||||
targets,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let minify_options = MinifyOptions {
|
||||
targets,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if lightsheet.minify(minify_options).is_err() {
|
||||
return Err(SabryCompilerError::Lightningcss());
|
||||
}
|
||||
|
||||
css = match lightsheet.to_css(printer_options) {
|
||||
Ok(c) => {
|
||||
drop(lightsheet);
|
||||
c.code
|
||||
}
|
||||
Err(_) => return Err(SabryCompilerError::Lightningcss()),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(css)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&SabryConfig> for grass::Options<'_> {
|
||||
fn from(config: &SabryConfig) -> Self {
|
||||
let mut options = grass::Options::default();
|
||||
options = options.style(if config.css.minify {
|
||||
grass::OutputStyle::Compressed
|
||||
} else {
|
||||
grass::OutputStyle::Expanded
|
||||
});
|
||||
options = options.load_path(&config.sass.intermediate_dir);
|
||||
|
||||
options
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SabryCompilerError {
|
||||
#[error("Could not compile into CSS with grass")]
|
||||
GrassCompile(#[from] Box<grass::Error>),
|
||||
#[error("Could not parse compiled CSS with lightningcss")]
|
||||
Lightningcss(),
|
||||
}
|
||||
|
||||
/// Unification wrapper over all the Syntax enums
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum CompilerSyntax {
|
||||
#[cfg(feature = "sass")]
|
||||
Sass,
|
||||
#[cfg(feature = "scss")]
|
||||
Scss,
|
||||
}
|
||||
|
||||
impl From<raffia::Syntax> for CompilerSyntax {
|
||||
fn from(value: raffia::Syntax) -> Self {
|
||||
match value {
|
||||
#[cfg(feature = "sass")]
|
||||
raffia::Syntax::Sass => Self::Sass,
|
||||
#[cfg(feature = "scss")]
|
||||
raffia::Syntax::Scss => Self::Scss,
|
||||
// TODO: fallbacky error-prone
|
||||
_ => Self::Scss,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CompilerSyntax> for grass::InputSyntax {
|
||||
fn from(value: CompilerSyntax) -> Self {
|
||||
match value {
|
||||
#[cfg(feature = "scss")]
|
||||
CompilerSyntax::Scss => Self::Scss,
|
||||
#[cfg(feature = "sass")]
|
||||
CompilerSyntax::Sass => Self::Sass,
|
||||
}
|
||||
}
|
||||
}
|
||||
44
sabry_intrnl/src/config/manifest.rs
Normal file
44
sabry_intrnl/src/config/manifest.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
use std::{env, fs, io};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::SabryConfig;
|
||||
|
||||
pub const MANIFEST: &str = "Cargo.toml";
|
||||
pub const MANIFEST_DIR_ENV: &str = "CARGO_MANIFEST_DIR";
|
||||
|
||||
pub fn read_manifest() -> Result<ValuableManifest, ManifestError> {
|
||||
let manpath = format!("{}/{MANIFEST}", env::var(MANIFEST_DIR_ENV)?);
|
||||
|
||||
Ok(toml::de::from_str(&fs::read_to_string(manpath)?)?)
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ManifestError {
|
||||
#[error("Could not read manifest path from env")]
|
||||
ManifestDirEnv(#[from] env::VarError),
|
||||
#[error("Could not read manifest file")]
|
||||
ManifestRead(#[from] io::Error),
|
||||
#[error("Could not deserialize manifest")]
|
||||
ManifestDe(#[from] toml::de::Error),
|
||||
}
|
||||
|
||||
/// Manifest structure that makes sense for sabry
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct ValuableManifest {
|
||||
pub package: Option<ValuableManifestPkg>,
|
||||
}
|
||||
|
||||
/// Manifest \[package.metadata\] structure that makes sense for sabry
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct ValuableManifestPkg {
|
||||
pub metadata: Option<ValuableManifestPkgMeta>,
|
||||
}
|
||||
|
||||
/// Manifest \[package.metadata.sabry\] structure that makes sense for sabry
|
||||
///
|
||||
/// Which is basically [SabryConfig]
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct ValuableManifestPkgMeta {
|
||||
pub sabry: Option<SabryConfig>,
|
||||
}
|
||||
253
sabry_intrnl/src/config/mod.rs
Normal file
253
sabry_intrnl/src/config/mod.rs
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
pub mod manifest;
|
||||
use manifest::{ManifestError, ValuableManifest};
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Sabry configuration, as it is from package.metadata.sabry
|
||||
#[derive(Default, Deserialize, Clone)]
|
||||
pub struct SabryConfig {
|
||||
#[serde(default = "SabryCssConfig::default")]
|
||||
pub css: SabryCssConfig,
|
||||
#[serde(default = "SabrySassConfig::default")]
|
||||
pub sass: SabrySassConfig,
|
||||
#[serde(default = "SabryHashConfig::default")]
|
||||
pub hash: SabryHashConfig,
|
||||
#[serde(default = "SabryGrassConfig::default")]
|
||||
pub grass: SabryGrassConfig,
|
||||
#[serde(default = "SabryLightCssConfig::default")]
|
||||
pub lightningcss: SabryLightCssConfig,
|
||||
}
|
||||
|
||||
impl SabryConfig {
|
||||
/// Require sabry configuration for the current project from every place in the code
|
||||
pub fn require() -> Result<Self, ManifestError> {
|
||||
let manifest = manifest::read_manifest()?;
|
||||
Ok(Self::from(&manifest))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ValuableManifest> for SabryConfig {
|
||||
fn from(value: &ValuableManifest) -> Self {
|
||||
value
|
||||
.package
|
||||
.as_ref()
|
||||
.and_then(|p| p.metadata.as_ref())
|
||||
.and_then(|m| m.sabry.as_ref())
|
||||
.map_or(Self::default(), |sbr| sbr.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Sabry `css` configuration table
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct SabryCssConfig {
|
||||
pub bundle: Option<String>,
|
||||
pub scopes: Option<String>,
|
||||
pub bundle_prelude: Option<Vec<String>>,
|
||||
#[serde(default = "SabryCssConfig::default_minify")]
|
||||
pub minify: bool,
|
||||
}
|
||||
|
||||
impl SabryCssConfig {
|
||||
fn default_minify() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SabryCssConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bundle: None,
|
||||
scopes: None,
|
||||
bundle_prelude: None,
|
||||
minify: Self::default_minify(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sabry `sass` configuration table
|
||||
///
|
||||
/// (SASS/SCSS related config)
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct SabrySassConfig {
|
||||
#[serde(default = "SabrySassConfig::default_scanroot")]
|
||||
pub scanroot: String,
|
||||
pub modules: Option<Vec<String>>,
|
||||
#[serde(default = "SabrySassConfig::default_intermediate_dir")]
|
||||
pub intermediate_dir: String,
|
||||
#[serde(default = "SabrySassConfig::default_module_name_collision")]
|
||||
pub module_name_collision: BehavSassModCollision,
|
||||
}
|
||||
|
||||
impl SabrySassConfig {
|
||||
fn default_scanroot() -> String {
|
||||
"src".into()
|
||||
}
|
||||
fn default_intermediate_dir() -> String {
|
||||
"target/.sabry/sass".into()
|
||||
}
|
||||
fn default_module_name_collision() -> BehavSassModCollision {
|
||||
BehavSassModCollision::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SabrySassConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scanroot: Self::default_scanroot(),
|
||||
modules: None,
|
||||
intermediate_dir: Self::default_intermediate_dir(),
|
||||
module_name_collision: Self::default_module_name_collision(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sabry `hash` configuration table
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct SabryHashConfig {
|
||||
#[serde(default = "SabryHashConfig::default_size")]
|
||||
pub size: usize,
|
||||
#[serde(default = "SabryHashConfig::default_collision")]
|
||||
pub collision: BehavHashCollision,
|
||||
#[serde(default = "SabryHashConfig::default_use_scope_name")]
|
||||
pub use_scope_name: bool,
|
||||
#[serde(default = "SabryHashConfig::default_use_item_names")]
|
||||
pub use_item_names: bool,
|
||||
#[serde(default = "SabryHashConfig::default_use_code_size")]
|
||||
pub use_code_size: bool,
|
||||
#[serde(default = "SabryHashConfig::default_use_code_text")]
|
||||
pub use_code_text: bool,
|
||||
}
|
||||
|
||||
impl SabryHashConfig {
|
||||
fn default_size() -> usize {
|
||||
6
|
||||
}
|
||||
fn default_collision() -> BehavHashCollision {
|
||||
BehavHashCollision::default()
|
||||
}
|
||||
fn default_use_scope_name() -> bool {
|
||||
true
|
||||
}
|
||||
fn default_use_item_names() -> bool {
|
||||
false
|
||||
}
|
||||
fn default_use_code_size() -> bool {
|
||||
true
|
||||
}
|
||||
fn default_use_code_text() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SabryHashConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
size: Self::default_size(),
|
||||
collision: Self::default_collision(),
|
||||
use_scope_name: Self::default_use_scope_name(),
|
||||
use_code_size: Self::default_use_code_size(),
|
||||
use_code_text: Self::default_use_code_text(),
|
||||
use_item_names: Self::default_use_item_names(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [grass] specific configuration for sabry
|
||||
#[derive(Default, Deserialize, Clone)]
|
||||
pub struct SabryGrassConfig {}
|
||||
|
||||
/// [lightningcss] specific configuration for sabry
|
||||
#[derive(Default, Deserialize, Clone)]
|
||||
pub struct SabryLightCssConfig {
|
||||
pub targets: SabryLightTargets,
|
||||
}
|
||||
|
||||
/// \[package.metadata.sabry.lightningcss.targets\] configuration.
|
||||
///
|
||||
/// This is a convenience layer under [lightningcss::targets::Browsers] structure accepted
|
||||
/// by lightningcss.
|
||||
///
|
||||
/// This structure is a part of [SabryConfig], which faces developer with more convenient
|
||||
/// `safari = "13.2"` syntax.
|
||||
#[derive(Default, Deserialize, Clone)]
|
||||
pub struct SabryLightTargets {
|
||||
pub android: Option<String>,
|
||||
pub chrome: Option<String>,
|
||||
pub edge: Option<String>,
|
||||
pub firefox: Option<String>,
|
||||
pub ie: Option<String>,
|
||||
pub ios_saf: Option<String>,
|
||||
pub opera: Option<String>,
|
||||
pub safari: Option<String>,
|
||||
pub samsung: Option<String>,
|
||||
}
|
||||
|
||||
impl SabryLightTargets {
|
||||
pub fn parse_ver(ver: &str) -> u32 {
|
||||
let mut vercode: u32 = 0;
|
||||
let vers = ver.split('.');
|
||||
for (i, dig) in vers.into_iter().enumerate() {
|
||||
let dignum = dig
|
||||
.parse::<u32>()
|
||||
.unwrap_or_else(|_| panic!("Invalid browser specifier: '{dig}'"));
|
||||
|
||||
let shift = 16 - 8 * i;
|
||||
vercode |= dignum << shift;
|
||||
}
|
||||
vercode
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SabryLightTargets> for lightningcss::targets::Browsers {
|
||||
fn from(value: SabryLightTargets) -> Self {
|
||||
Self {
|
||||
android: value.android.map(|s| SabryLightTargets::parse_ver(&s)),
|
||||
chrome: value.chrome.map(|s| SabryLightTargets::parse_ver(&s)),
|
||||
edge: value.edge.map(|s| SabryLightTargets::parse_ver(&s)),
|
||||
firefox: value.firefox.map(|s| SabryLightTargets::parse_ver(&s)),
|
||||
ie: value.ie.map(|s| SabryLightTargets::parse_ver(&s)),
|
||||
ios_saf: value.ios_saf.map(|s| SabryLightTargets::parse_ver(&s)),
|
||||
opera: value.opera.map(|s| SabryLightTargets::parse_ver(&s)),
|
||||
safari: value.safari.map(|s| SabryLightTargets::parse_ver(&s)),
|
||||
samsung: value.samsung.map(|s| SabryLightTargets::parse_ver(&s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// \[package.metadata.sabry.sass\].module_name_collision option
|
||||
#[derive(Deserialize, Clone, Copy)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum BehavSassModCollision {
|
||||
Merge,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl Default for BehavSassModCollision {
|
||||
fn default() -> Self {
|
||||
Self::Merge
|
||||
}
|
||||
}
|
||||
|
||||
/// \[package.metadata.sabry.hash\].collision option
|
||||
#[derive(Deserialize, Clone, Copy)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum BehavHashCollision {
|
||||
Error,
|
||||
Ignore,
|
||||
}
|
||||
|
||||
impl Default for BehavHashCollision {
|
||||
fn default() -> Self {
|
||||
Self::Ignore
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{manifest, SabryConfig};
|
||||
|
||||
#[test]
|
||||
fn empty_manifest_still_worth_a_read() {
|
||||
let manifest = manifest::read_manifest().unwrap();
|
||||
let _conf = SabryConfig::from(&manifest);
|
||||
}
|
||||
}
|
||||
4
sabry_intrnl/src/lib.rs
Normal file
4
sabry_intrnl/src/lib.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pub mod compiler;
|
||||
pub mod config;
|
||||
pub mod scoper;
|
||||
pub mod syntax;
|
||||
97
sabry_intrnl/src/scoper/hash.rs
Normal file
97
sabry_intrnl/src/scoper/hash.rs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
use base64::Engine;
|
||||
|
||||
use crate::config::SabryHashConfig;
|
||||
|
||||
use super::ArbitraryScope;
|
||||
|
||||
/// Convenience wrapper for String-being-a-hash
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ScopeHash(String);
|
||||
|
||||
impl ScopeHash {
|
||||
pub fn new(scope: &ArbitraryScope, config: &SabryHashConfig) -> Self {
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
|
||||
if config.use_scope_name {
|
||||
hasher.update(scope.name.to_string().as_bytes());
|
||||
}
|
||||
if config.use_code_text {
|
||||
hasher.update(scope.adapter().source().as_bytes());
|
||||
}
|
||||
if config.use_code_size {
|
||||
hasher.update(&scope.adapter().source().len().to_ne_bytes());
|
||||
}
|
||||
if config.use_item_names {
|
||||
let classes = scope.adapter().class_selectors();
|
||||
let ids = scope.adapter().id_selectors();
|
||||
//# types/tags dont participate
|
||||
//# child-parent dont participate
|
||||
|
||||
let merged_items = classes
|
||||
.iter()
|
||||
.map(|s| &s.name)
|
||||
.chain(ids.iter().map(|i| &i.name))
|
||||
.map(|ident| ident.as_literal())
|
||||
.filter_map(|mbl| mbl.map(|l| l.raw))
|
||||
.collect::<String>();
|
||||
|
||||
hasher.update(merged_items.as_bytes());
|
||||
}
|
||||
|
||||
let size = if config.size >= blake3::OUT_LEN {
|
||||
blake3::OUT_LEN
|
||||
} else {
|
||||
config.size
|
||||
};
|
||||
|
||||
let hash = hasher.finalize();
|
||||
let hash = hash.as_bytes();
|
||||
|
||||
//? This isn't right. "size" is size of hash in symbols.
|
||||
// naaah, its fine :D (Dan)
|
||||
let hash = base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(&hash[..size]);
|
||||
|
||||
Self(hash)
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn test_init(v: String) -> Self {
|
||||
Self(v)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use syn::Ident;
|
||||
|
||||
use crate::{config::SabryHashConfig, scoper::ArbitraryScope};
|
||||
|
||||
use super::ScopeHash;
|
||||
|
||||
#[test]
|
||||
fn hash_matches() {
|
||||
let source = ".cls1{color:red; &-dark{color: black} #id1 {color:green;} div {color:blue;}} .cls3#id2{color: black;}";
|
||||
|
||||
let scope1 = ArbitraryScope::from_source(
|
||||
raffia::Syntax::Scss,
|
||||
syn::parse_str::<Ident>("lasifudm").unwrap(),
|
||||
source,
|
||||
)
|
||||
.unwrap();
|
||||
let hash1 = ScopeHash::new(&scope1, &SabryHashConfig::default());
|
||||
|
||||
let scope2 = ArbitraryScope::from_source(
|
||||
raffia::Syntax::Scss,
|
||||
syn::parse_str::<Ident>("lasifudm").unwrap(),
|
||||
source,
|
||||
)
|
||||
.unwrap();
|
||||
let hash2 = ScopeHash::new(&scope2, &SabryHashConfig::default());
|
||||
|
||||
assert_eq!(hash1, hash2);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
226
sabry_intrnl/src/syntax/mod.rs
Normal file
226
sabry_intrnl/src/syntax/mod.rs
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
use raffia::{
|
||||
ast::{
|
||||
ClassSelector, CompoundSelector, IdSelector, NestingSelector, SimpleSelector, Statement,
|
||||
Stylesheet, TypeSelector,
|
||||
},
|
||||
ParserBuilder, Syntax,
|
||||
};
|
||||
|
||||
/// Convenience wrapper for [Stylesheet]
|
||||
pub struct StylesheetAdapter<'s> {
|
||||
pub syntax: Syntax,
|
||||
source: &'s str,
|
||||
stylesheet: Stylesheet<'s>,
|
||||
}
|
||||
|
||||
impl<'s> StylesheetAdapter<'s> {
|
||||
pub fn new(source: &'s str, syntax: Syntax) -> Result<Self, raffia::error::Error> {
|
||||
Ok(Self {
|
||||
stylesheet: ParserBuilder::new(source)
|
||||
.ignore_comments()
|
||||
.syntax(syntax)
|
||||
.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())
|
||||
}
|
||||
|
||||
/// 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 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, raffia::Syntax::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, raffia::Syntax::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, raffia::Syntax::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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue