Compare commits
19 commits
main
...
feat/v4-ru
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1ec791afd | ||
|
|
a88d3f2043 | ||
|
|
3f199d0745 | ||
|
|
86e824aeee | ||
|
|
bbcf9ecfe0 | ||
|
|
7f19f7ef75 | ||
|
|
eeb38e7399 | ||
|
|
ff0a5e3f7b | ||
|
|
8c5aec02c4 | ||
|
|
25a88c57bb | ||
|
|
50bb8dbfe1 | ||
|
|
5b078a975a | ||
|
|
1491289ac0 | ||
|
|
17598c5c2c | ||
|
|
2dda298272 | ||
|
|
191bc824c2 | ||
|
|
045887cd62 | ||
|
|
7d2b791efb | ||
|
|
5d82f5b472 |
41 changed files with 5814 additions and 125 deletions
833
Cargo.lock
generated
833
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
27
crates/core/Cargo.toml
Normal file
27
crates/core/Cargo.toml
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
[package]
|
||||||
|
name = "tailwindcss-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
opt-level = "s"
|
||||||
|
panic = "abort"
|
||||||
|
strip = true
|
||||||
|
codegen-units = 1
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde_json = "1.0.127"
|
||||||
|
log = "0.4.22"
|
||||||
|
wasm-bindgen = "0.2.93"
|
||||||
|
bstr = "1.10.0"
|
||||||
|
memmem = "0.1.1"
|
||||||
|
memchr = "2.7.4"
|
||||||
|
tinyvec = { version = "1.8.0", features = ["alloc"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rstest = "0.22.0"
|
||||||
|
rstest_reuse = "0.7.0"
|
||||||
5
crates/core/src/compat/config.rs
Normal file
5
crates/core/src/compat/config.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
pub struct UserConfig {
|
||||||
|
internal: Value,
|
||||||
|
}
|
||||||
1
crates/core/src/compat/mod.rs
Normal file
1
crates/core/src/compat/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod config;
|
||||||
7
crates/core/src/compat/plugin.rs
Normal file
7
crates/core/src/compat/plugin.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
pub struct Plugin {
|
||||||
|
/// An internal identifier that the server uses to identify the plugin.
|
||||||
|
/// This identifier is guaranteed to be unique for the lifetime of the
|
||||||
|
/// plugin server but is not guaranteed to be unique across multiple
|
||||||
|
/// invocations of the server.
|
||||||
|
handle: u64
|
||||||
|
}
|
||||||
18
crates/core/src/compiler.rs
Normal file
18
crates/core/src/compiler.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use crate::css::parser::parse;
|
||||||
|
use crate::css::ast::Stylesheet;
|
||||||
|
|
||||||
|
struct Compiler {
|
||||||
|
ast: Stylesheet,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Compiler {
|
||||||
|
fn new(css: &[u8]) -> Result<Compiler, Box<dyn Error>> {
|
||||||
|
let ast = parse(css)?;
|
||||||
|
|
||||||
|
Ok(Compiler {
|
||||||
|
ast,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
156
crates/core/src/css/ast.rs
Normal file
156
crates/core/src/css/ast.rs
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
use std::{collections::HashMap, fmt, unreachable};
|
||||||
|
|
||||||
|
/// Represents the AST of a CSS stylesheet
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct Stylesheet {
|
||||||
|
pub(crate) rules: CssNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A node in a CSS Stylesheet
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub enum CssNode {
|
||||||
|
/// A context block used to provide shared data to subtrees
|
||||||
|
Context {
|
||||||
|
data: HashMap<String, String>,
|
||||||
|
nodes: Vec<CssNode>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A CSS at rule
|
||||||
|
AtRule {
|
||||||
|
name: Vec<u8>,
|
||||||
|
params: Vec<u8>,
|
||||||
|
nodes: Vec<CssNode>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A CSS style rule
|
||||||
|
StyleRule {
|
||||||
|
selector: Vec<u8>,
|
||||||
|
nodes: Vec<CssNode>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A CSS declaration
|
||||||
|
Declaration {
|
||||||
|
property: Vec<u8>,
|
||||||
|
value: Vec<u8>,
|
||||||
|
important: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A comment
|
||||||
|
Comment {
|
||||||
|
value: Vec<u8>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Represents multiple CSS nodes
|
||||||
|
/// This is used when replacing a single node with multiple nodes
|
||||||
|
Contents {
|
||||||
|
nodes: Vec<CssNode>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn style_rule(selector: impl Into<Vec<u8>>, nodes: impl Into<Vec<CssNode>>) -> CssNode {
|
||||||
|
CssNode::StyleRule {
|
||||||
|
selector: selector.into(),
|
||||||
|
nodes: nodes.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn at_rule(name: impl Into<Vec<u8>>, params: impl Into<Vec<u8>>, nodes: impl Into<Vec<CssNode>>) -> CssNode {
|
||||||
|
CssNode::AtRule {
|
||||||
|
name: name.into(),
|
||||||
|
params: params.into(),
|
||||||
|
nodes: nodes.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn decl(property: impl Into<Vec<u8>>, value: impl Into<Vec<u8>>, important: bool) -> CssNode {
|
||||||
|
CssNode::Declaration {
|
||||||
|
property: property.into(),
|
||||||
|
value: value.into(),
|
||||||
|
important,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn comment(value: impl Into<Vec<u8>>) -> CssNode {
|
||||||
|
CssNode::Comment { value: value.into() }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<T> for CssNode where T: Into<Vec<CssNode>> {
|
||||||
|
fn from(nodes: T) -> Self {
|
||||||
|
CssNode::Contents { nodes: nodes.into() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<T> for Stylesheet where T: Into<Vec<CssNode>> {
|
||||||
|
fn from(nodes: T) -> Self {
|
||||||
|
Stylesheet {
|
||||||
|
rules: CssNode::from(nodes.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CssNode {
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
CssNode::from([])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Stylesheet {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "Stylesheet {{ rules: {:?} }}", self.rules)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for CssNode {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
CssNode::Context { data, nodes } => {
|
||||||
|
write!(f, "Context {{ data: {:?}, nodes: {:?} }}", data, nodes)
|
||||||
|
},
|
||||||
|
|
||||||
|
CssNode::AtRule { name, params, nodes } => {
|
||||||
|
write!(f, "AtRule {{ name: {:?}, params: {:?}, nodes: {:?} }}", String::from_utf8_lossy(name), String::from_utf8_lossy(params), nodes)
|
||||||
|
},
|
||||||
|
|
||||||
|
CssNode::StyleRule { selector, nodes } => {
|
||||||
|
write!(f, "StyleRule {{ selector: {:?}, nodes: {:?} }}", String::from_utf8_lossy(selector), nodes)
|
||||||
|
},
|
||||||
|
|
||||||
|
CssNode::Declaration { property, value, important } => {
|
||||||
|
write!(f, "Declaration {{ property: {:?}, value: {:?}, important: {:?} }}", String::from_utf8_lossy(property), String::from_utf8_lossy(value), important)
|
||||||
|
},
|
||||||
|
|
||||||
|
CssNode::Comment { value } => {
|
||||||
|
write!(f, "Comment {{ value: {:?} }}", String::from_utf8_lossy(value))
|
||||||
|
},
|
||||||
|
|
||||||
|
CssNode::Contents { nodes } => {
|
||||||
|
write!(f, "Contents {{ nodes: {:?} }}", nodes)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CssNode {
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn push(&mut self, node: CssNode) {
|
||||||
|
match self {
|
||||||
|
CssNode::AtRule { nodes, .. } => {
|
||||||
|
nodes.push(node);
|
||||||
|
},
|
||||||
|
CssNode::StyleRule { nodes, .. } => {
|
||||||
|
nodes.push(node);
|
||||||
|
},
|
||||||
|
CssNode::Contents { nodes, .. } => {
|
||||||
|
nodes.push(node);
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
panic!("Cannot push to a non-container node.");
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
crates/core/src/css/mod.rs
Normal file
6
crates/core/src/css/mod.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
pub mod ast;
|
||||||
|
pub mod optimize;
|
||||||
|
pub mod parser;
|
||||||
|
pub mod serializer;
|
||||||
|
pub mod syntax;
|
||||||
|
pub mod visit;
|
||||||
306
crates/core/src/css/optimize.rs
Normal file
306
crates/core/src/css/optimize.rs
Normal file
|
|
@ -0,0 +1,306 @@
|
||||||
|
// Performs an optimization pass on the AST to (usually) reduce the size of the output CSS
|
||||||
|
|
||||||
|
use std::{collections::HashSet, mem};
|
||||||
|
|
||||||
|
use super::{ast::{at_rule, decl, style_rule, CssNode, Stylesheet}, visit::WalkAction};
|
||||||
|
|
||||||
|
pub fn optimize_ast(ast: &mut Stylesheet) {
|
||||||
|
remove_duplicate_at_properties(ast);
|
||||||
|
add_property_fallbacks(ast);
|
||||||
|
hoist_at_roots(ast);
|
||||||
|
flatten_utilities(ast);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove duplicate `@property` rules appearing in the AST
|
||||||
|
/// They're replaced with empty nodes that print nothing
|
||||||
|
fn remove_duplicate_at_properties(ast: &mut Stylesheet) {
|
||||||
|
let mut seen = HashSet::<Vec<u8>>::new();
|
||||||
|
|
||||||
|
ast.walk_mut(&mut |node| {
|
||||||
|
let CssNode::AtRule { name, params, .. } = node else {
|
||||||
|
return WalkAction::Continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if name != b"property" {
|
||||||
|
return WalkAction::Continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !seen.contains(params) {
|
||||||
|
seen.insert(params.clone());
|
||||||
|
|
||||||
|
return WalkAction::Continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
*node = CssNode::empty();
|
||||||
|
|
||||||
|
return WalkAction::Skip;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect fallbacks for `@property` rules for Firefox support
|
||||||
|
/// We turn these into rules on `:root` or `*` and some pseudo-elements
|
||||||
|
/// based on the value of `inherits`
|
||||||
|
fn add_property_fallbacks(ast: &mut Stylesheet) {
|
||||||
|
let mut fallbacks_root = Vec::<CssNode>::new();
|
||||||
|
let mut fallbacks_universal = Vec::<CssNode>::new();
|
||||||
|
|
||||||
|
// Create fallback rules for defined properties
|
||||||
|
ast.walk_mut(&mut |node| {
|
||||||
|
let CssNode::AtRule { name, params, nodes, .. } = node else {
|
||||||
|
return WalkAction::Continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if name != b"property" {
|
||||||
|
return WalkAction::Continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let property_name = params.clone();
|
||||||
|
let mut initial_value: Option<Vec<u8>> = None;
|
||||||
|
let mut inherits = false;
|
||||||
|
|
||||||
|
for child in nodes {
|
||||||
|
let CssNode::Declaration { property, value, .. } = child else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if property == b"initial-value" {
|
||||||
|
initial_value = Some(value.clone());
|
||||||
|
} else if property == b"inherits" {
|
||||||
|
inherits = value == b"true";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let initial_value = initial_value.unwrap_or(b"initial".to_vec());
|
||||||
|
|
||||||
|
if inherits {
|
||||||
|
fallbacks_root.push(decl(property_name, initial_value, false));
|
||||||
|
} else {
|
||||||
|
fallbacks_universal.push(decl(property_name, initial_value, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
return WalkAction::Skip;
|
||||||
|
});
|
||||||
|
|
||||||
|
let CssNode::Contents { nodes } = &mut ast.rules else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut fallback_ast = vec![];
|
||||||
|
|
||||||
|
if !fallbacks_root.is_empty() {
|
||||||
|
fallback_ast.push(style_rule(b":root", fallbacks_root));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fallbacks_universal.is_empty() {
|
||||||
|
fallback_ast.push(style_rule(
|
||||||
|
b"*, ::before, ::after, ::backdrop",
|
||||||
|
fallbacks_universal
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fallback_ast.is_empty() {
|
||||||
|
fallback_ast = vec![
|
||||||
|
at_rule(b"supports", b"(-moz-orient: inline)", [
|
||||||
|
at_rule(b"layer", b"base", fallback_ast),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.extend(fallback_ast);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect fallbacks for `@property` rules for Firefox support
|
||||||
|
/// We turn these into rules on `:root` or `*` and some pseudo-elements
|
||||||
|
/// based on the value of `inherits`
|
||||||
|
fn hoist_at_roots(ast: &mut Stylesheet) {
|
||||||
|
let mut roots = Vec::<CssNode>::new();
|
||||||
|
|
||||||
|
ast.walk_mut(&mut |node| {
|
||||||
|
let CssNode::AtRule { name, nodes, .. } = node else {
|
||||||
|
return WalkAction::Continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if name != b"at-root" {
|
||||||
|
return WalkAction::Continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull the nodes out of the at-root rule
|
||||||
|
roots.extend(mem::take(nodes));
|
||||||
|
|
||||||
|
// Replace the at-root rule with an empty node
|
||||||
|
*node = CssNode::empty();
|
||||||
|
|
||||||
|
return WalkAction::Skip;
|
||||||
|
});
|
||||||
|
|
||||||
|
let CssNode::Contents { nodes } = &mut ast.rules else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes.extend(roots);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect fallbacks for `@property` rules for Firefox support
|
||||||
|
/// We turn these into rules on `:root` or `*` and some pseudo-elements
|
||||||
|
/// based on the value of `inherits`
|
||||||
|
fn flatten_utilities(ast: &mut Stylesheet) {
|
||||||
|
ast.walk_mut(&mut |node| {
|
||||||
|
let CssNode::AtRule { name, params, nodes, .. } = node else {
|
||||||
|
return WalkAction::Continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if name != b"tailwind" {
|
||||||
|
return WalkAction::Continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if params != b"utilities" {
|
||||||
|
return WalkAction::Continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
*node = CssNode::from(mem::take(nodes));
|
||||||
|
|
||||||
|
return WalkAction::Skip;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_remove_duplicate_at_properties() {
|
||||||
|
let mut css = Stylesheet::from([
|
||||||
|
at_rule("property", "--foo", [
|
||||||
|
decl("syntax", "<length>", false),
|
||||||
|
decl("inherits", "false", false),
|
||||||
|
decl("initial-value", "0", false),
|
||||||
|
]),
|
||||||
|
|
||||||
|
at_rule("property", "--foo", [
|
||||||
|
decl("syntax", "<length>", false),
|
||||||
|
decl("inherits", "false", false),
|
||||||
|
decl("initial-value", "0", false),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
remove_duplicate_at_properties(&mut css);
|
||||||
|
|
||||||
|
let expected = Stylesheet::from([
|
||||||
|
at_rule("property", "--foo", [
|
||||||
|
decl("syntax", "<length>", false),
|
||||||
|
decl("inherits", "false", false),
|
||||||
|
decl("initial-value", "0", false),
|
||||||
|
]),
|
||||||
|
|
||||||
|
CssNode::empty(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(css, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_add_property_fallbacks() {
|
||||||
|
let mut css = Stylesheet::from([
|
||||||
|
at_rule("property", "--foo", [
|
||||||
|
decl("syntax", "<length>", false),
|
||||||
|
decl("inherits", "true", false),
|
||||||
|
decl("initial-value", "0", false),
|
||||||
|
]),
|
||||||
|
|
||||||
|
at_rule("property", "--bar", [
|
||||||
|
decl("syntax", "<length>", false),
|
||||||
|
decl("inherits", "false", false),
|
||||||
|
decl("initial-value", "0", false),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
add_property_fallbacks(&mut css);
|
||||||
|
|
||||||
|
let expected = Stylesheet::from([
|
||||||
|
at_rule("property", "--foo", [
|
||||||
|
decl("syntax", "<length>", false),
|
||||||
|
decl("inherits", "true", false),
|
||||||
|
decl("initial-value", "0", false),
|
||||||
|
]),
|
||||||
|
|
||||||
|
at_rule("property", "--bar", [
|
||||||
|
decl("syntax", "<length>", false),
|
||||||
|
decl("inherits", "false", false),
|
||||||
|
decl("initial-value", "0", false),
|
||||||
|
]),
|
||||||
|
|
||||||
|
at_rule("supports", "(-moz-orient: inline)", [
|
||||||
|
at_rule("layer", "base", [
|
||||||
|
style_rule(":root", [
|
||||||
|
decl("--foo", "0", false),
|
||||||
|
]),
|
||||||
|
|
||||||
|
style_rule("*, ::before, ::after, ::backdrop", [
|
||||||
|
decl("--bar", "0", false),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(css, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hoist_at_roots() {
|
||||||
|
let mut css = Stylesheet::from([
|
||||||
|
at_rule("layer", "base", [
|
||||||
|
at_rule("at-root", "", [
|
||||||
|
at_rule("keyframes", "spin", [
|
||||||
|
style_rule("from", [
|
||||||
|
decl("transform", "rotate(0deg)", false),
|
||||||
|
]),
|
||||||
|
|
||||||
|
style_rule("to", [
|
||||||
|
decl("transform", "rotate(360deg)", false),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
|
||||||
|
at_rule("layer", "defaults", [
|
||||||
|
at_rule("at-root", "", [
|
||||||
|
at_rule("keyframes", "pulse", [
|
||||||
|
style_rule("50%", [
|
||||||
|
decl("opacity", "0", false),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
hoist_at_roots(&mut css);
|
||||||
|
|
||||||
|
let expected = Stylesheet::from([
|
||||||
|
at_rule("layer", "base", [
|
||||||
|
CssNode::empty(),
|
||||||
|
|
||||||
|
at_rule("layer", "defaults", [
|
||||||
|
CssNode::empty(),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
|
||||||
|
at_rule("keyframes", "spin", [
|
||||||
|
style_rule("from", [
|
||||||
|
decl("transform", "rotate(0deg)", false),
|
||||||
|
]),
|
||||||
|
|
||||||
|
style_rule("to", [
|
||||||
|
decl("transform", "rotate(360deg)", false),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
|
||||||
|
at_rule("keyframes", "pulse", [
|
||||||
|
style_rule("50%", [
|
||||||
|
decl("opacity", "0", false),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(css, expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
1704
crates/core/src/css/parser.rs
Normal file
1704
crates/core/src/css/parser.rs
Normal file
File diff suppressed because it is too large
Load diff
159
crates/core/src/css/serializer.rs
Normal file
159
crates/core/src/css/serializer.rs
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
use super::ast::{CssNode, Stylesheet};
|
||||||
|
|
||||||
|
impl Stylesheet {
|
||||||
|
pub fn to_css(&self) -> Vec<u8> {
|
||||||
|
self.rules.to_css()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CssNode {
|
||||||
|
fn to_css(&self) -> Vec<u8> {
|
||||||
|
let mut css: Vec<u8> = vec![];
|
||||||
|
self.write_css_to(&mut css, 0);
|
||||||
|
return css;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_css_to(&self, css: &mut Vec<u8>, depth: usize) {
|
||||||
|
let indent = b" ".repeat(depth);
|
||||||
|
|
||||||
|
match self {
|
||||||
|
CssNode::Comment { value } => {
|
||||||
|
css.extend(&indent);
|
||||||
|
css.extend(b"/*");
|
||||||
|
css.extend(value);
|
||||||
|
css.extend(b"*/\n");
|
||||||
|
},
|
||||||
|
|
||||||
|
CssNode::Declaration { property, value, important } => {
|
||||||
|
css.extend(&indent);
|
||||||
|
css.extend(property);
|
||||||
|
css.extend(b": ");
|
||||||
|
css.extend(value);
|
||||||
|
if *important {
|
||||||
|
css.extend(b" !important");
|
||||||
|
}
|
||||||
|
css.extend(b";\n");
|
||||||
|
},
|
||||||
|
|
||||||
|
CssNode::Context { nodes, .. } => {
|
||||||
|
for child in nodes {
|
||||||
|
child.write_css_to(css, depth);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
CssNode::Contents { nodes } => {
|
||||||
|
for child in nodes {
|
||||||
|
child.write_css_to(css, depth);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
CssNode::AtRule { name, params, nodes } => {
|
||||||
|
css.extend(&indent);
|
||||||
|
css.extend(b"@");
|
||||||
|
css.extend(name);
|
||||||
|
css.extend(b" ");
|
||||||
|
css.extend(params);
|
||||||
|
|
||||||
|
// Print at-rules without nodes with a `;` instead of an empty block.
|
||||||
|
//
|
||||||
|
// E.g.:
|
||||||
|
//
|
||||||
|
// ```css
|
||||||
|
// @layer base, components, utilities;
|
||||||
|
// ```
|
||||||
|
if nodes.is_empty() {
|
||||||
|
css.extend(b";\n");
|
||||||
|
} else {
|
||||||
|
css.extend(b" {\n");
|
||||||
|
|
||||||
|
for child in nodes {
|
||||||
|
child.write_css_to(css, depth + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
css.extend(&indent);
|
||||||
|
css.extend(b"}\n");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
CssNode::StyleRule { selector, nodes } => {
|
||||||
|
css.extend(&indent);
|
||||||
|
css.extend(selector);
|
||||||
|
css.extend(b" {\n");
|
||||||
|
|
||||||
|
for child in nodes {
|
||||||
|
child.write_css_to(css, depth + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
css.extend(&indent);
|
||||||
|
css.extend(b"}\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::css::{ast::{comment, decl}, parser::parse};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_pretty_print_an_ast() {
|
||||||
|
let css = parse(b".foo{color:red;&:hover{color:blue;}}").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(css.to_css(), b".foo {\n color: red;\n &:hover {\n color: blue;\n }\n}\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_print_decls() {
|
||||||
|
let css = decl(b"color", "red", false);
|
||||||
|
assert_eq!(css.to_css(), b"color: red;\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_print_decls_important() {
|
||||||
|
let css = decl(b"color", "red", true);
|
||||||
|
assert_eq!(css.to_css(), b"color: red !important;\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_print_comments() {
|
||||||
|
let css = comment(b" hello world ");
|
||||||
|
|
||||||
|
assert_eq!(css.to_css(), b"/* hello world */\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_print_at_rules_without_a_body() {
|
||||||
|
let css = parse(b"@layer base, components, utilities;").unwrap();
|
||||||
|
assert_eq!(css.to_css(), b"@layer base, components, utilities;\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_print_at_rules_with_a_body() {
|
||||||
|
let css = parse(b"@layer base { color: red; }").unwrap();
|
||||||
|
assert_eq!(css.to_css(), b"@layer base {\n color: red;\n}\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_print_at_rules_with_a_body_and_nested_rules() {
|
||||||
|
let css = parse(b"@layer base { color: red; &:hover { color: blue; } }").unwrap();
|
||||||
|
assert_eq!(css.to_css(), b"@layer base {\n color: red;\n &:hover {\n color: blue;\n }\n}\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_print_style_rules() {
|
||||||
|
let css = parse(b".foo { color: red; }").unwrap();
|
||||||
|
assert_eq!(css.to_css(), b".foo {\n color: red;\n}\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_print_style_rules_with_nested_style_rules() {
|
||||||
|
let css = parse(b".foo { color: red; &:hover { color: blue; } }").unwrap();
|
||||||
|
assert_eq!(css.to_css(), b".foo {\n color: red;\n &:hover {\n color: blue;\n }\n}\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_print_style_rules_with_nested_at_rules() {
|
||||||
|
let css = parse(b".foo { color: red; @layer base { color: blue; } }").unwrap();
|
||||||
|
assert_eq!(css.to_css(), b".foo {\n color: red;\n @layer base {\n color: blue;\n }\n}\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
56
crates/core/src/css/syntax.rs
Normal file
56
crates/core/src/css/syntax.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
// The order of these cases is important for performance
|
||||||
|
// please do not change it without significant profiling
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
enum Case {
|
||||||
|
Escape,
|
||||||
|
Ident,
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
const __CASES: [Case; 256] = {
|
||||||
|
let mut table = [Case::Other; 256];
|
||||||
|
|
||||||
|
let mut i = b'a';
|
||||||
|
while i <= b'z' {
|
||||||
|
table[i as usize] = Case::Ident;
|
||||||
|
i+=1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut i = b'A';
|
||||||
|
while i <= b'Z' {
|
||||||
|
table[i as usize] = Case::Ident;
|
||||||
|
i+=1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut i = b'0';
|
||||||
|
while i <= b'9' {
|
||||||
|
table[i as usize] = Case::Ident;
|
||||||
|
i+=1;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[b'-' as usize] = Case::Ident;
|
||||||
|
table[b'_' as usize] = Case::Ident;
|
||||||
|
table[b'\\' as usize] = Case::Escape;
|
||||||
|
|
||||||
|
table
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Consume an <ident-token> from the buffer
|
||||||
|
///
|
||||||
|
/// Not intended to be as strict as the [CSS spec][diagram] but merely good enough.
|
||||||
|
/// [diagram]: https://drafts.csswg.org/css-syntax-3/#ident-token-diagram
|
||||||
|
#[inline(never)]
|
||||||
|
#[no_mangle]
|
||||||
|
pub fn read_ident_token(buffer: &[u8]) -> usize {
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
|
while i < buffer.len() {
|
||||||
|
match __CASES[buffer[i] as usize] {
|
||||||
|
Case::Ident => i += 1,
|
||||||
|
Case::Escape => i += 2,
|
||||||
|
Case::Other => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return i;
|
||||||
|
}
|
||||||
192
crates/core/src/css/visit.rs
Normal file
192
crates/core/src/css/visit.rs
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
use super::ast::{CssNode, Stylesheet};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum WalkAction {
|
||||||
|
// Continue walking, which is the default
|
||||||
|
Continue,
|
||||||
|
|
||||||
|
// Skip visiting the children of this node
|
||||||
|
Skip,
|
||||||
|
|
||||||
|
// Stop the walk entirely
|
||||||
|
Stop,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stylesheet {
|
||||||
|
pub fn walk<V>(&self, cb: &V)
|
||||||
|
where
|
||||||
|
V: Fn(&CssNode) -> WalkAction
|
||||||
|
{
|
||||||
|
self.rules.walk(cb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CssNode {
|
||||||
|
pub fn walk<V>(&self, cb: &V)
|
||||||
|
where
|
||||||
|
V: Fn(&CssNode) -> WalkAction
|
||||||
|
{
|
||||||
|
_ = self.walk_impl(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn walk_impl<V>(&self, cb: &V) -> WalkAction
|
||||||
|
where
|
||||||
|
V: Fn(&CssNode) -> WalkAction
|
||||||
|
{
|
||||||
|
match cb(self) {
|
||||||
|
WalkAction::Stop => return WalkAction::Stop,
|
||||||
|
WalkAction::Skip => return WalkAction::Skip,
|
||||||
|
WalkAction::Continue => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
for node in self.children() {
|
||||||
|
match node.walk_impl(cb) {
|
||||||
|
WalkAction::Stop => return WalkAction::Stop,
|
||||||
|
WalkAction::Skip => {}
|
||||||
|
WalkAction::Continue => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WalkAction::Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> impl Iterator<Item = &CssNode> {
|
||||||
|
match self {
|
||||||
|
CssNode::Context { nodes, .. } => nodes.iter(),
|
||||||
|
CssNode::AtRule { nodes, .. } => nodes.iter(),
|
||||||
|
CssNode::StyleRule { nodes, .. } => nodes.iter(),
|
||||||
|
CssNode::Contents { nodes, .. } => nodes.iter(),
|
||||||
|
CssNode::Declaration { .. } => [].iter(),
|
||||||
|
CssNode::Comment { .. } => [].iter(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stylesheet {
|
||||||
|
pub fn walk_mut<V>(&mut self, cb: &mut V)
|
||||||
|
where
|
||||||
|
V: FnMut(&mut CssNode) -> WalkAction
|
||||||
|
{
|
||||||
|
self.rules.walk_mut(cb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CssNode {
|
||||||
|
pub fn walk_mut<V>(&mut self, cb: &mut V)
|
||||||
|
where
|
||||||
|
V: FnMut(&mut CssNode) -> WalkAction
|
||||||
|
{
|
||||||
|
_ = self.walk_mut_impl(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn walk_mut_impl<V>(&mut self, cb: &mut V) -> WalkAction
|
||||||
|
where
|
||||||
|
V: FnMut(&mut CssNode) -> WalkAction
|
||||||
|
{
|
||||||
|
match cb(self) {
|
||||||
|
WalkAction::Stop => return WalkAction::Stop,
|
||||||
|
WalkAction::Skip => return WalkAction::Skip,
|
||||||
|
WalkAction::Continue => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
for node in self.children_mut() {
|
||||||
|
match node.walk_mut_impl(cb) {
|
||||||
|
WalkAction::Stop => return WalkAction::Stop,
|
||||||
|
WalkAction::Skip => {}
|
||||||
|
WalkAction::Continue => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WalkAction::Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn children_mut(&mut self) -> impl Iterator<Item = &mut CssNode> {
|
||||||
|
match self {
|
||||||
|
CssNode::Context { nodes, .. } => nodes.iter_mut(),
|
||||||
|
CssNode::AtRule { nodes, .. } => nodes.iter_mut(),
|
||||||
|
CssNode::StyleRule { nodes, .. } => nodes.iter_mut(),
|
||||||
|
CssNode::Contents { nodes, .. } => nodes.iter_mut(),
|
||||||
|
CssNode::Declaration { .. } => [].iter_mut(),
|
||||||
|
CssNode::Comment { .. } => [].iter_mut(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::css::ast::*;
|
||||||
|
use std::cell::Cell;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_walk() {
|
||||||
|
let ast = Stylesheet::from([
|
||||||
|
style_rule("h1", [
|
||||||
|
decl("color", "red", false),
|
||||||
|
decl("font-size", "2em", false),
|
||||||
|
]),
|
||||||
|
style_rule("h2", [
|
||||||
|
decl("color", "blue", false),
|
||||||
|
decl("font-size", "1.5em", false),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let count = Cell::new(0);
|
||||||
|
|
||||||
|
ast.walk(&|node| {
|
||||||
|
if let CssNode::Declaration { property, .. } = node {
|
||||||
|
if property == b"color" {
|
||||||
|
count.set(count.get() + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WalkAction::Continue
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(count.get(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_walk_mut() {
|
||||||
|
let mut ast = Stylesheet::from([
|
||||||
|
style_rule("h1", [
|
||||||
|
decl("color", "red", false),
|
||||||
|
decl("font-size", "2em", false),
|
||||||
|
]),
|
||||||
|
style_rule("h2", [
|
||||||
|
decl("color", "blue", false),
|
||||||
|
decl("font-size", "1.5em", false),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 1. Change all color properties to green
|
||||||
|
ast.walk_mut(&mut |node| {
|
||||||
|
let CssNode::Declaration { property, value, .. } = node else {
|
||||||
|
return WalkAction::Continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if property != b"color" {
|
||||||
|
return WalkAction::Continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
*value = "green".into();
|
||||||
|
|
||||||
|
return WalkAction::Continue;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Re-walk the AST and check that all color properties are green
|
||||||
|
let count = Cell::new(0);
|
||||||
|
|
||||||
|
ast.walk(&|node| {
|
||||||
|
if let CssNode::Declaration { property, value, .. } = node {
|
||||||
|
if property == b"color" && value == b"green" {
|
||||||
|
count.set(count.get() + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WalkAction::Continue
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(count.get(), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
329
crates/core/src/engine/candidate.rs
Normal file
329
crates/core/src/engine/candidate.rs
Normal file
|
|
@ -0,0 +1,329 @@
|
||||||
|
use std::{iter::empty, rc::Rc, sync::Arc};
|
||||||
|
use bstr::ByteSlice;
|
||||||
|
|
||||||
|
use crate::util::segment;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Candidate {
|
||||||
|
raw: Vec<u8>,
|
||||||
|
important: bool,
|
||||||
|
variants: Vec<Variant>,
|
||||||
|
utilities: Vec<Utility>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Variant {
|
||||||
|
/// Arbitrary variants are variants that take a selector and generate a variant
|
||||||
|
/// on the fly.
|
||||||
|
///
|
||||||
|
/// E.g.: `[&_p]`
|
||||||
|
Arbitrary {
|
||||||
|
selector: Vec<u8>,
|
||||||
|
|
||||||
|
/// If true, it can be applied as a child of a compound variant
|
||||||
|
compounds: bool,
|
||||||
|
|
||||||
|
/// Whether or not the selector is a relative selector
|
||||||
|
/// @see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors/Selector_structure#relative_selector
|
||||||
|
relative: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Static variants are variants that don't take any arguments.
|
||||||
|
///
|
||||||
|
/// E.g.: `hover`
|
||||||
|
Static {
|
||||||
|
root: Vec<u8>,
|
||||||
|
compounds: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Functional variants are variants that can take an argument. The argument is
|
||||||
|
/// either a named variant value or an arbitrary variant value.
|
||||||
|
///
|
||||||
|
/// E.g.:
|
||||||
|
///
|
||||||
|
/// - `aria-disabled`
|
||||||
|
/// - `aria-[disabled]`
|
||||||
|
/// - `@container-size` -> @container, with named value `size`
|
||||||
|
/// - `@container-[inline-size]` -> @container, with arbitrary variant value `inline-size`
|
||||||
|
/// - `@container` -> @container, with no value
|
||||||
|
Functional {
|
||||||
|
root: Vec<u8>,
|
||||||
|
value: Option<VariantValue>,
|
||||||
|
modifier: Option<CandidateModifier>,
|
||||||
|
|
||||||
|
/// If true, it can be applied as a child of a compound variant
|
||||||
|
compounds: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Compound variants are variants that take another variant as an argument.
|
||||||
|
///
|
||||||
|
/// E.g.:
|
||||||
|
///
|
||||||
|
/// - `has-[&_p]`
|
||||||
|
/// - `group-*`
|
||||||
|
/// - `peer-*`
|
||||||
|
Compound {
|
||||||
|
root: Vec<u8>,
|
||||||
|
variant: Box<Variant>,
|
||||||
|
modifier: Option<CandidateModifier>,
|
||||||
|
|
||||||
|
/// If true, it can be applied as a child of a compound variant
|
||||||
|
compounds: bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Utility {
|
||||||
|
/// Arbitrary candidates are candidates that register utilities on the fly with
|
||||||
|
/// a property and a value.
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
/// - `[color:red]`
|
||||||
|
/// - `[color:red]/50`
|
||||||
|
/// - `[color:red]/50!`
|
||||||
|
Arbitrary {
|
||||||
|
property: Vec<u8>,
|
||||||
|
value: Vec<u8>,
|
||||||
|
modifier: Option<CandidateModifier>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Static candidates are candidates that don't take any arguments.
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
/// - `underline`
|
||||||
|
/// - `flex`
|
||||||
|
Static {
|
||||||
|
root: Vec<u8>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Static candidates are candidates that don't take any arguments.
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
/// - `underline`
|
||||||
|
/// - `flex`
|
||||||
|
Functional {
|
||||||
|
root: Vec<u8>,
|
||||||
|
value: Option<UtilityValue>,
|
||||||
|
modifier: Option<CandidateModifier>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum UtilityValue {
|
||||||
|
Arbitrary {
|
||||||
|
/// bg-[color:--my-color]
|
||||||
|
/// ^^^^^
|
||||||
|
data_type: Option<Vec<u8>>,
|
||||||
|
|
||||||
|
/// bg-[#0088cc]
|
||||||
|
/// ^^^^^^^
|
||||||
|
/// bg-[var(--my_variable)]
|
||||||
|
/// ^^^^^^^^^^^^^^^^^^
|
||||||
|
value: Vec<u8>,
|
||||||
|
},
|
||||||
|
|
||||||
|
Named {
|
||||||
|
/// bg-red-500
|
||||||
|
/// ^^^^^^^
|
||||||
|
///
|
||||||
|
/// w-1/2
|
||||||
|
/// ^
|
||||||
|
value: Vec<u8>,
|
||||||
|
|
||||||
|
/// w-1/2
|
||||||
|
/// ^^^
|
||||||
|
fraction: Option<Vec<u8>>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum VariantValue {
|
||||||
|
Arbitrary {
|
||||||
|
value: Vec<u8>,
|
||||||
|
},
|
||||||
|
|
||||||
|
Named {
|
||||||
|
value: Vec<u8>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum CandidateModifier {
|
||||||
|
Arbitrary {
|
||||||
|
/// bg-red-500/[50%]
|
||||||
|
/// ^^^
|
||||||
|
value: Vec<u8>
|
||||||
|
},
|
||||||
|
|
||||||
|
Named {
|
||||||
|
/// bg-red-500/50
|
||||||
|
/// ^^
|
||||||
|
value: Vec<u8>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DesignSystem {
|
||||||
|
prefix: Option<Vec<u8>>,
|
||||||
|
utilities: Utilities,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Utilities {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Utilities {
|
||||||
|
pub fn has(&self, utility: &[u8]) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_candidate(input: &[u8], design: Rc<DesignSystem>) -> Option<Candidate> {
|
||||||
|
let raw = input.to_vec();
|
||||||
|
|
||||||
|
// hover:focus:underline
|
||||||
|
// ^^^^^ ^^^^^^ -> Variants
|
||||||
|
// ^^^^^^^^^ -> Base
|
||||||
|
let mut raw_variants = segment(input, b':');
|
||||||
|
|
||||||
|
if let Some(prefix) = &design.prefix {
|
||||||
|
let Some(new_variants) = raw_variants.strip_prefix(prefix) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
if new_variants.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
raw_variants = new_variants.to_vec();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety: At this point it is safe to use TypeScript's non-null assertion
|
||||||
|
// operator because even if the `input` was an empty string, splitting an
|
||||||
|
// empty string by `:` will always result in an array with at least one
|
||||||
|
// element.
|
||||||
|
let mut base = raw_variants.pop().unwrap();
|
||||||
|
|
||||||
|
let mut parsed_variants: Vec<Variant> = Vec::with_capacity(raw_variants.len());
|
||||||
|
|
||||||
|
for i in (0..raw_variants.len()).rev() {
|
||||||
|
let parsed_variant = parse_variant(raw_variants[i]);
|
||||||
|
if parsed_variant.is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed_variants.push(parsed_variant.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut important = false;
|
||||||
|
let mut negative = false;
|
||||||
|
|
||||||
|
// Candidates that end with an exclamation mark are the important version with
|
||||||
|
// higher specificity of the non-important candidate, e.g. `mx-4!`.
|
||||||
|
if let Some(new_base) = base.strip_suffix(b"!") {
|
||||||
|
important = true;
|
||||||
|
base = new_base;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy syntax with leading `!`, e.g. `!mx-4`.
|
||||||
|
else if let Some(new_base) = base.strip_prefix(b"!") {
|
||||||
|
important = true;
|
||||||
|
base = new_base;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Candidates that start with a dash are the negative versions of another
|
||||||
|
// candidate, e.g. `-mx-4`.
|
||||||
|
if let Some(new_base) = base.strip_prefix(b"-") {
|
||||||
|
negative = true;
|
||||||
|
base = new_base;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut utilities: Vec<Utility> = vec![];
|
||||||
|
|
||||||
|
// Check for an exact match of a static utility first as long as it does not
|
||||||
|
// look like an arbitrary value.
|
||||||
|
if design.utilities.has(base) && !base.contains(&b'[') {
|
||||||
|
utilities.push(Utility::Static {
|
||||||
|
root: base.to_vec(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Figure out the new base and the modifier segment if present.
|
||||||
|
//
|
||||||
|
// E.g.:
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// bg-red-500/50
|
||||||
|
// ^^^^^^^^^^ -> Base without modifier
|
||||||
|
// ^^ -> Modifier segment
|
||||||
|
// ```
|
||||||
|
let parts = segment(base, b'/');
|
||||||
|
|
||||||
|
// If there's more than one modifier, the utility is invalid.
|
||||||
|
//
|
||||||
|
// E.g.:
|
||||||
|
//
|
||||||
|
// - `bg-red-500/50/50`
|
||||||
|
if parts.len() > 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// let [baseWithoutModifier, modifierSegment = null, additionalModifier] = segment(base, '/')
|
||||||
|
|
||||||
|
|
||||||
|
Some(Candidate {
|
||||||
|
raw: raw.to_vec(),
|
||||||
|
important,
|
||||||
|
variants: parsed_variants.to_vec(),
|
||||||
|
utilities,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_variant(input: &[u8]) -> Option<Variant> {
|
||||||
|
return Some(Variant::Static { root: vec![], compounds: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_arbitrary_property(base: &[u8]) -> Option<Utility> {
|
||||||
|
// Arbitrary properties must start and end with square brackets.
|
||||||
|
let Some(base) = base.strip_prefix(b"[") else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(base) = base.strip_suffix(b"]") else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The property part of the arbitrary property can only start with a-z
|
||||||
|
// lowercase or a dash `-` in case of vendor prefixes such as `-webkit-`
|
||||||
|
// or `-moz-`.
|
||||||
|
//
|
||||||
|
// Otherwise, it is an invalid candidate, and skip continue parsing.
|
||||||
|
if base[0] != b'-' && !(base[0] >= b'a' && base[0] <= b'z') {
|
||||||
|
return None
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arbitrary properties consist of a property and a value separated by a
|
||||||
|
// `:`. If the `:` cannot be found, then it is an invalid candidate, and we
|
||||||
|
// can skip continue parsing.
|
||||||
|
//
|
||||||
|
// Since the property and the value should be separated by a `:`, we can
|
||||||
|
// also verify that the colon is not the first or last character in the
|
||||||
|
// candidate, because that would make it invalid as well.
|
||||||
|
let Some(idx) = base.find(b":") else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
if idx == 0 || idx == base.len() - 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let property = base[..idx].to_vec();
|
||||||
|
let value = base[idx+1..].to_vec();
|
||||||
|
|
||||||
|
// let value = decodeArbitraryValue(base.slice(idx + 1))
|
||||||
|
|
||||||
|
Some(Utility::Arbitrary {
|
||||||
|
property,
|
||||||
|
value,
|
||||||
|
modifier: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
10
crates/core/src/engine/mod.rs
Normal file
10
crates/core/src/engine/mod.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
// mod candidate;
|
||||||
|
|
||||||
|
/// This represents the core logic of:
|
||||||
|
/// - Parsing a candidate
|
||||||
|
/// - Matching that against a list of known utilities and known variants
|
||||||
|
/// - Returning the appropriate "functions" to be called
|
||||||
|
|
||||||
|
struct Engine {
|
||||||
|
//
|
||||||
|
}
|
||||||
139
crates/core/src/lib.rs
Normal file
139
crates/core/src/lib.rs
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
mod compat;
|
||||||
|
mod css;
|
||||||
|
mod util;
|
||||||
|
mod compiler;
|
||||||
|
mod engine;
|
||||||
|
|
||||||
|
use css::optimize::optimize_ast;
|
||||||
|
use css::parser::parse;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use std::cell::Cell;
|
||||||
|
use css::ast::CssNode;
|
||||||
|
use css::visit::WalkAction;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn wip() {
|
||||||
|
// 1. Parse the CSS
|
||||||
|
let mut ast = parse(b"h1 { color: red; font-size: 2em; } h2 { color: blue; font-size: 1.5em; }").unwrap();
|
||||||
|
|
||||||
|
// 2. Walk the AST and mutate all color properties to green
|
||||||
|
ast.walk_mut(&mut |node| {
|
||||||
|
let CssNode::Declaration { property, value, .. } = node else {
|
||||||
|
return WalkAction::Continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if property != b"color" {
|
||||||
|
return WalkAction::Continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
*value = "green".into();
|
||||||
|
|
||||||
|
return WalkAction::Continue;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Walk the AST and count the number of font-size properties
|
||||||
|
let count = Cell::new(0);
|
||||||
|
|
||||||
|
ast.walk(&|node| {
|
||||||
|
let CssNode::Declaration { property, .. } = node else {
|
||||||
|
return WalkAction::Continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if property != b"font-size" {
|
||||||
|
return WalkAction::Continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
count.set(count.get() + 1);
|
||||||
|
|
||||||
|
WalkAction::Continue
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Optimize the AST
|
||||||
|
optimize_ast(&mut ast);
|
||||||
|
|
||||||
|
// 5. Serialize the AST back to CSS
|
||||||
|
let css = ast.to_css();
|
||||||
|
|
||||||
|
// 6. Print the CSS
|
||||||
|
println!("{}", String::from_utf8_lossy(&css));
|
||||||
|
println!("{}", count.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
// use css::ast::{decl, rule, Ast, AstNode, WalkAction};
|
||||||
|
|
||||||
|
// pub struct Compiler {
|
||||||
|
// ast: Stylesheet,
|
||||||
|
// config_paths: Vec<String>,
|
||||||
|
// plugin_paths: Vec<String>,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl Compiler {
|
||||||
|
// fn new(css: &[u8]) -> Compiler {
|
||||||
|
// let mut ast = parse_css(css);
|
||||||
|
// let mut plugin_paths = vec![];
|
||||||
|
// let mut config_paths = vec![];
|
||||||
|
|
||||||
|
// ast.walk(&mut |node, _| {
|
||||||
|
// let AstNode::Rule { selector, .. } = node else {
|
||||||
|
// return WalkAction::Continue;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// if selector.starts_with(b"@plugin") {
|
||||||
|
// let path = selector.split_at(7).1;
|
||||||
|
// let path = &path[2..path.len()-1];
|
||||||
|
// let path = path.to_vec();
|
||||||
|
|
||||||
|
// plugin_paths.push(unsafe {
|
||||||
|
// String::from_utf8_unchecked(path)
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if selector.starts_with(b"@config") {
|
||||||
|
// let path = selector.split_at(7).1;
|
||||||
|
// let path = &path[2..path.len()-1];
|
||||||
|
// let path = path.to_vec();
|
||||||
|
|
||||||
|
// config_paths.push(unsafe {
|
||||||
|
// String::from_utf8_unchecked(path)
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// WalkAction::Continue
|
||||||
|
// });
|
||||||
|
|
||||||
|
// Compiler {
|
||||||
|
// ast,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // fn plugin_paths(&self) -> Vec<String> {
|
||||||
|
// // vec![]
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// // fn config_paths(&self) -> Vec<String> {
|
||||||
|
// // vec![]
|
||||||
|
// // }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn parse_css(css: &[u8]) -> Ast {
|
||||||
|
// // TODO: Parse the CSS into an AST
|
||||||
|
// _ = css;
|
||||||
|
|
||||||
|
// Ast::from(vec![
|
||||||
|
// rule(b"body", vec![
|
||||||
|
// decl(b"color", Some(b"red"), false),
|
||||||
|
// ]),
|
||||||
|
// ])
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn foo() {
|
||||||
|
// let css = b"body { color: red; }";
|
||||||
|
// let compiler = Compiler::new(css);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// enum UtilityDescriptor {
|
||||||
|
// Simple {
|
||||||
|
// name: String,
|
||||||
|
// ast: Ast,
|
||||||
|
// },
|
||||||
|
// }
|
||||||
380
crates/core/src/main.rs
Normal file
380
crates/core/src/main.rs
Normal file
|
|
@ -0,0 +1,380 @@
|
||||||
|
use std::hint::black_box;
|
||||||
|
|
||||||
|
const PREFLIGHT: &'static str = r#"/*
|
||||||
|
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||||
|
2. Remove default margins and padding
|
||||||
|
3. Reset all borders.
|
||||||
|
*/
|
||||||
|
|
||||||
|
*,
|
||||||
|
::after,
|
||||||
|
::before,
|
||||||
|
::backdrop,
|
||||||
|
::file-selector-button {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
margin: 0; /* 2 */
|
||||||
|
padding: 0; /* 2 */
|
||||||
|
border: 0 solid; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Use a consistent sensible line-height in all browsers.
|
||||||
|
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||||
|
3. Use a more readable tab size.
|
||||||
|
4. Use the user's configured `sans` font-family by default.
|
||||||
|
5. Use the user's configured `sans` font-feature-settings by default.
|
||||||
|
6. Use the user's configured `sans` font-variation-settings by default.
|
||||||
|
7. Disable tap highlights on iOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html,
|
||||||
|
:host {
|
||||||
|
line-height: 1.5; /* 1 */
|
||||||
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
|
tab-size: 4; /* 3 */
|
||||||
|
font-family: var(
|
||||||
|
--default-font-family,
|
||||||
|
ui-sans-serif,
|
||||||
|
system-ui,
|
||||||
|
sans-serif,
|
||||||
|
'Apple Color Emoji',
|
||||||
|
'Segoe UI Emoji',
|
||||||
|
'Segoe UI Symbol',
|
||||||
|
'Noto Color Emoji'
|
||||||
|
); /* 4 */
|
||||||
|
font-feature-settings: var(--default-font-feature-settings, normal); /* 5 */
|
||||||
|
font-variation-settings: var(--default-font-variation-settings, normal); /* 6 */
|
||||||
|
-webkit-tap-highlight-color: transparent; /* 7 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Add the correct height in Firefox.
|
||||||
|
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||||
|
3. Reset the default border style to a 1px solid border.
|
||||||
|
*/
|
||||||
|
|
||||||
|
hr {
|
||||||
|
height: 0; /* 1 */
|
||||||
|
color: inherit; /* 2 */
|
||||||
|
border-top-width: 1px; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abbr:where([title]) {
|
||||||
|
-webkit-text-decoration: underline dotted;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the default font size and weight for headings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Reset links to optimize for opt-in styling instead of opt-out.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
-webkit-text-decoration: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct font weight in Edge and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Use the user's configured `mono` font-family by default.
|
||||||
|
2. Use the user's configured `mono` font-feature-settings by default.
|
||||||
|
3. Use the user's configured `mono` font-variation-settings by default.
|
||||||
|
4. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp,
|
||||||
|
pre {
|
||||||
|
font-family: var(
|
||||||
|
--default-mono-font-family,
|
||||||
|
ui-monospace,
|
||||||
|
SFMono-Regular,
|
||||||
|
Menlo,
|
||||||
|
Monaco,
|
||||||
|
Consolas,
|
||||||
|
'Liberation Mono',
|
||||||
|
'Courier New',
|
||||||
|
monospace
|
||||||
|
); /* 4 */
|
||||||
|
font-feature-settings: var(--default-mono-font-feature-settings, normal); /* 5 */
|
||||||
|
font-variation-settings: var(--default-mono-font-variation-settings, normal); /* 6 */
|
||||||
|
font-size: 1em; /* 4 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||||
|
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||||
|
3. Remove gaps between table borders by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
table {
|
||||||
|
text-indent: 0; /* 1 */
|
||||||
|
border-color: inherit; /* 2 */
|
||||||
|
border-collapse: collapse; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Inherit the font styles in all browsers.
|
||||||
|
2. Remove the default background color.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea,
|
||||||
|
::file-selector-button {
|
||||||
|
font: inherit; /* 1 */
|
||||||
|
font-feature-settings: inherit; /* 1 */
|
||||||
|
font-variation-settings: inherit; /* 1 */
|
||||||
|
letter-spacing: inherit; /* 1 */
|
||||||
|
color: inherit; /* 1 */
|
||||||
|
background: transparent; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Reset the default inset border style for form controls to solid.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input:where(:not([type='button'], [type='reset'], [type='submit'])),
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
border: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Correct the inability to style the border radius in iOS Safari.
|
||||||
|
*/
|
||||||
|
button,
|
||||||
|
input:where([type='button'], [type='reset'], [type='submit']),
|
||||||
|
::file-selector-button {
|
||||||
|
appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Use the modern Firefox focus style for all focusable elements.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:-moz-focusring {
|
||||||
|
outline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||||
|
*/
|
||||||
|
|
||||||
|
:-moz-ui-invalid {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct vertical alignment in Chrome and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Correct the cursor style of increment and decrement buttons in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-inner-spin-button,
|
||||||
|
::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the inner padding in Chrome and Safari on macOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct display in Chrome and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Make lists unstyled by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
menu {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Prevent resizing textareas horizontally by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||||
|
2. Set the default placeholder color to a semi-transparent version of the current text color.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::placeholder {
|
||||||
|
opacity: 1; /* 1 */
|
||||||
|
color: color-mix(in srgb, currentColor 50%, transparent); /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||||
|
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||||
|
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg,
|
||||||
|
video,
|
||||||
|
canvas,
|
||||||
|
audio,
|
||||||
|
iframe,
|
||||||
|
embed,
|
||||||
|
object {
|
||||||
|
display: block; /* 1 */
|
||||||
|
vertical-align: middle; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||||
|
*/
|
||||||
|
|
||||||
|
img,
|
||||||
|
video {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Make elements with the HTML hidden attribute stay hidden by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
mod css;
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
let throughput = util::Throughput::compute(100_000, PREFLIGHT.len(), || {
|
||||||
|
_ = black_box(css::parse(PREFLIGHT.as_bytes()));
|
||||||
|
});
|
||||||
|
|
||||||
|
eprintln!("css::parse: {:}", throughput);
|
||||||
|
|
||||||
|
let input = &b"var(--a, 0 0 1px rgb(0, 0, 0)), 0 0 1px rgb(0, 0, 0), var(--a, 0 0 1px rgb(0, 0, 0)), 0 0 1px rgb(0, 0, 0), ".repeat(500)[..];
|
||||||
|
|
||||||
|
let throughput = util::Throughput::compute(100_000, input.len(), || {
|
||||||
|
_ = black_box(util::segment(input, b','));
|
||||||
|
});
|
||||||
|
|
||||||
|
eprintln!("util::segment: {:}", throughput);
|
||||||
|
|
||||||
|
let input = &b"s\\o\\m\\e\\_\\identifier_token ".repeat(500)[..];
|
||||||
|
|
||||||
|
let throughput = util::Throughput::compute(100_000_000, input.len(), || {
|
||||||
|
_ = black_box(css::syntax::read_ident_token(input));
|
||||||
|
});
|
||||||
|
|
||||||
|
eprintln!("css::syntax::read_ident_token: {:}", throughput);
|
||||||
|
|
||||||
|
let input = &b"@media (min-width: 1280px) ".repeat(500)[..];
|
||||||
|
|
||||||
|
let throughput = util::Throughput::compute(2_000_000, input.len(), || {
|
||||||
|
_ = black_box(css::parser::parse_rule_header(input));
|
||||||
|
});
|
||||||
|
|
||||||
|
eprintln!("css::parser::parse_rule_header (at rule): {:}", throughput);
|
||||||
|
|
||||||
|
let input = &b".foo.bar:is(.baz:has(.qux:where(.foo + .bar))) + .thing::before".repeat(500)[..];
|
||||||
|
|
||||||
|
let throughput = util::Throughput::compute(2_000_000, input.len(), || {
|
||||||
|
_ = black_box(css::parser::parse_rule_header(input));
|
||||||
|
});
|
||||||
|
|
||||||
|
eprintln!("css::parser::parse_rule_header (style rule): {:}", throughput);
|
||||||
|
|
||||||
|
let input = &b"[content-start]_calc(100%-1px)_[content-end]_minmax(1rem,1fr)".repeat(500)[..];
|
||||||
|
|
||||||
|
let throughput = util::Throughput::compute(100_000, input.len(), || {
|
||||||
|
_ = black_box(util::convert_underscores_to_whitespace(input));
|
||||||
|
});
|
||||||
|
|
||||||
|
eprintln!("util::convert_underscores_to_whitespace: {:}", throughput);
|
||||||
|
}
|
||||||
122
crates/core/src/util/decode.rs
Normal file
122
crates/core/src/util/decode.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
// import { addWhitespaceAroundMathOperators } from './math-operators'
|
||||||
|
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
pub fn decode_arbitrary_value(input: &[u8]) -> Vec<u8> {
|
||||||
|
// We do not want to normalize anything inside of a url() because if we
|
||||||
|
// replace `_` with ` `, then it will very likely break the url.
|
||||||
|
if input.starts_with(b"url(") {
|
||||||
|
return input.to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
let input = convert_underscores_to_whitespace(input);
|
||||||
|
// let input = addWhitespaceAroundMathOperators(input);
|
||||||
|
|
||||||
|
return input.to_vec();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert `_` to ` ` unless escaped (`\_`) in which case they
|
||||||
|
/// should be converted to `_` instead.
|
||||||
|
pub fn convert_underscores_to_whitespace(input: &[u8]) -> Vec<u8> {
|
||||||
|
let mut result = Vec::<u8>::with_capacity(input.len());
|
||||||
|
|
||||||
|
let input = write_decoded_8(input, &mut result);
|
||||||
|
write_decoded_scalar(input, &mut result);
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_decoded_8<'a, 'b>(input: &'a [u8], result: &'b mut Vec<u8>) -> &'a [u8] {
|
||||||
|
const CHUNK_SIZE: usize = mem::size_of::<u64>();
|
||||||
|
const NUL_8: [u8; CHUNK_SIZE] = [0x00; CHUNK_SIZE];
|
||||||
|
const SPACE_8: [u8; CHUNK_SIZE] = [b' '; CHUNK_SIZE];
|
||||||
|
const ESCAPE_8: [u8; CHUNK_SIZE] = [b'\\'; CHUNK_SIZE];
|
||||||
|
const UNDERSCORE_8: [u8; CHUNK_SIZE] = [b'_'; CHUNK_SIZE];
|
||||||
|
|
||||||
|
let mut chunks = input.chunks_exact(CHUNK_SIZE);
|
||||||
|
|
||||||
|
while let Some(chunk) = chunks.next() {
|
||||||
|
let mut chunk: [u8; CHUNK_SIZE] = chunk.try_into().unwrap();
|
||||||
|
let mut chunk: u64 = u64::from_ne_bytes(chunk);
|
||||||
|
|
||||||
|
let mut is_escape = [false; CHUNK_SIZE];
|
||||||
|
for j in 0..CHUNK_SIZE {
|
||||||
|
is_escape[j] = chunk[j] == b'\\';
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut is_underscore = [false; CHUNK_SIZE];
|
||||||
|
for j in 0..CHUNK_SIZE {
|
||||||
|
is_underscore[j] = chunk[j] == b'_';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace underscores with spaces in the chunk
|
||||||
|
for j in 0..CHUNK_SIZE {
|
||||||
|
chunk[j] = if is_underscore[j] {
|
||||||
|
SPACE_8[j]
|
||||||
|
} else {
|
||||||
|
chunk[j]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace escaped underscores with underscores
|
||||||
|
for j in 0..(CHUNK_SIZE - 1) {
|
||||||
|
chunk[j] = if is_escape[j] && is_underscore[j + 1] {
|
||||||
|
UNDERSCORE_8[j]
|
||||||
|
} else {
|
||||||
|
chunk[j]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace escapes with NUL bytes
|
||||||
|
for j in 0..CHUNK_SIZE {
|
||||||
|
chunk[j] = if is_escape[j] {
|
||||||
|
NUL_8[j]
|
||||||
|
} else {
|
||||||
|
chunk[j]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
result.extend(&chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks.remainder();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn write_decoded_scalar(input: &[u8], result: &mut Vec<u8>) {
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
|
while i < input.len() {
|
||||||
|
match input[i] {
|
||||||
|
b'\\' => {
|
||||||
|
if i + 1 == input.len() {
|
||||||
|
// We've hit the end of the string and there's no character to escape
|
||||||
|
result.extend(b"\\");
|
||||||
|
} else if input[i + 1] == b'_' {
|
||||||
|
// We've hit an escaped underscore
|
||||||
|
result.extend(b"_");
|
||||||
|
} else {
|
||||||
|
// We've hit an "escaped" character that isn't an underscore
|
||||||
|
// which means its not actually escaped and should be treated
|
||||||
|
// as a literal character. Since we've already read the next
|
||||||
|
// character, we'll just write both of them out here.
|
||||||
|
result.extend(&input[i..i+1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 2;
|
||||||
|
},
|
||||||
|
|
||||||
|
b'_' => {
|
||||||
|
result.push(b' ');
|
||||||
|
i += 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
result.push(input[i]);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// a\b\c
|
||||||
216
crates/core/src/util/escape.rs
Normal file
216
crates/core/src/util/escape.rs
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum Case {
|
||||||
|
Other,
|
||||||
|
Ident,
|
||||||
|
Nul,
|
||||||
|
Control,
|
||||||
|
}
|
||||||
|
|
||||||
|
// This list allows us to quickly identify what kind of byte we are looking at
|
||||||
|
// and jump to the right block of code to handle it producing a roughly 2x
|
||||||
|
// speedup in the general case.
|
||||||
|
static __CASES: [Case; 256] = {
|
||||||
|
let mut cases = [Case::Other; 256];
|
||||||
|
|
||||||
|
cases[0x00] = Case::Nul;
|
||||||
|
|
||||||
|
let mut i = 0x01;
|
||||||
|
while i <= 0x1f {
|
||||||
|
cases[i] = Case::Control;
|
||||||
|
i+=1;
|
||||||
|
}
|
||||||
|
|
||||||
|
cases[0x7f] = Case::Control;
|
||||||
|
|
||||||
|
let mut i = b'0';
|
||||||
|
while i <= b'9' {
|
||||||
|
cases[i as usize] = Case::Ident;
|
||||||
|
i+=1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut i = b'A';
|
||||||
|
while i <= b'Z' {
|
||||||
|
cases[i as usize] = Case::Ident;
|
||||||
|
i+=1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut i = b'a';
|
||||||
|
while i <= b'z' {
|
||||||
|
cases[i as usize] = Case::Ident;
|
||||||
|
i+=1;
|
||||||
|
}
|
||||||
|
|
||||||
|
cases[b'-' as usize] = Case::Ident;
|
||||||
|
cases[b'_' as usize] = Case::Ident;
|
||||||
|
|
||||||
|
let mut i = 0x80;
|
||||||
|
while i <= 0xff {
|
||||||
|
cases[i] = Case::Ident;
|
||||||
|
i+=1;
|
||||||
|
}
|
||||||
|
|
||||||
|
cases
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://drafts.csswg.org/cssom/#serialize-an-identifier
|
||||||
|
pub fn escape(value: &[u8]) -> Vec<u8> {
|
||||||
|
if value.len() == 0 {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
if value == b"-" {
|
||||||
|
return b"\\-".to_vec();
|
||||||
|
}
|
||||||
|
|
||||||
|
// While the worst-case is 4x the size of the input, the "average" worst case
|
||||||
|
// is actually 2x the size of the input. This would happen for a string
|
||||||
|
// consisting of all printable, non-ident characters. If we pre-allocate
|
||||||
|
// for this case we can avoid all re-allocations during the loop unless we
|
||||||
|
// happen to have enough control characters to trigger the worst-case.
|
||||||
|
let mut result: Vec<u8> = Vec::with_capacity(2 * value.len());
|
||||||
|
|
||||||
|
let mut value = value;
|
||||||
|
|
||||||
|
if value[0] == b'-' {
|
||||||
|
result.push(b'-');
|
||||||
|
value = &value[1..];
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: We're guaranteed to have at least one byte in `value` at this point
|
||||||
|
// because if len() > 0 AND the only byte is `-` then we've already handled
|
||||||
|
// that case.
|
||||||
|
if let digit @ b'0'..=b'9' = *unsafe { value.get_unchecked(0) } {
|
||||||
|
write_hex_digit(digit, &mut result);
|
||||||
|
value = &value[1..];
|
||||||
|
}
|
||||||
|
|
||||||
|
write_escape_scalar(value, &mut result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn write_escape_scalar(value: &[u8], result: &mut Vec<u8>) {
|
||||||
|
let replacement = "\u{FFFD}".as_bytes();
|
||||||
|
|
||||||
|
// Note: there’s no need to special-case astral symbols, surrogate
|
||||||
|
// pairs, or lone surrogates.
|
||||||
|
for &code_unit in value.iter() {
|
||||||
|
match __CASES[code_unit as usize] {
|
||||||
|
// Every character in /[0-9a-zA-Z-_]/ can be included directly
|
||||||
|
Case::Ident => result.push(code_unit),
|
||||||
|
|
||||||
|
// Other printable ASCII characters should be printed with an escape
|
||||||
|
// https://drafts.csswg.org/cssom/#escape-a-character
|
||||||
|
Case::Other => result.extend([
|
||||||
|
b'\\',
|
||||||
|
code_unit,
|
||||||
|
]),
|
||||||
|
|
||||||
|
// The NUL character (U+0000) becomes the REPLACEMENT CHARACTER (U+FFFD)
|
||||||
|
Case::Nul => result.extend(replacement),
|
||||||
|
|
||||||
|
// Control characters (U+0001–U+001F and U+007F) are written in hex
|
||||||
|
// https://drafts.csswg.org/cssom/#escape-a-character-as-code-point
|
||||||
|
Case::Control => write_hex_digit(code_unit, result),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_hex_digit(value: u8, s: &mut Vec<u8>) {
|
||||||
|
static HEX: &[u8; 16] = b"0123456789abcdef";
|
||||||
|
|
||||||
|
if value > 0x0F {
|
||||||
|
let hi = value >> 4 & 0x0F;
|
||||||
|
let lo = value >> 0 & 0x0F;
|
||||||
|
s.extend([
|
||||||
|
b'\\',
|
||||||
|
HEX[hi as usize],
|
||||||
|
HEX[lo as usize],
|
||||||
|
b' ',
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
s.extend([
|
||||||
|
b'\\',
|
||||||
|
HEX[value as usize],
|
||||||
|
b' ',
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test() {
|
||||||
|
assert_eq!(escape(b"\0"), "\u{FFFD}".as_bytes());
|
||||||
|
assert_eq!(escape(b"a\0"), "a\u{FFFD}".as_bytes());
|
||||||
|
assert_eq!(escape(b"\0b"), "\u{FFFD}b".as_bytes());
|
||||||
|
assert_eq!(escape(b"a\0b"), "a\u{FFFD}b".as_bytes());
|
||||||
|
|
||||||
|
assert_eq!(escape("\u{FFFD}".as_bytes()), "\u{FFFD}".as_bytes());
|
||||||
|
assert_eq!(escape("a\u{FFFD}".as_bytes()), "a\u{FFFD}".as_bytes());
|
||||||
|
assert_eq!(escape("\u{FFFD}b".as_bytes()), "\u{FFFD}b".as_bytes());
|
||||||
|
assert_eq!(escape("a\u{FFFD}b".as_bytes()), "a\u{FFFD}b".as_bytes());
|
||||||
|
|
||||||
|
assert_eq!(escape(b""), b"");
|
||||||
|
|
||||||
|
assert_eq!(escape(b"\x01\x02\x1E\x1F"), b"\\1 \\2 \\1e \\1f ");
|
||||||
|
|
||||||
|
assert_eq!(escape(b"0a"), b"\\30 a");
|
||||||
|
assert_eq!(escape(b"1a"), b"\\31 a");
|
||||||
|
assert_eq!(escape(b"2a"), b"\\32 a");
|
||||||
|
assert_eq!(escape(b"3a"), b"\\33 a");
|
||||||
|
assert_eq!(escape(b"4a"), b"\\34 a");
|
||||||
|
assert_eq!(escape(b"5a"), b"\\35 a");
|
||||||
|
assert_eq!(escape(b"6a"), b"\\36 a");
|
||||||
|
assert_eq!(escape(b"7a"), b"\\37 a");
|
||||||
|
assert_eq!(escape(b"8a"), b"\\38 a");
|
||||||
|
assert_eq!(escape(b"9a"), b"\\39 a");
|
||||||
|
|
||||||
|
assert_eq!(escape(b"a0b"), b"a0b");
|
||||||
|
assert_eq!(escape(b"a1b"), b"a1b");
|
||||||
|
assert_eq!(escape(b"a2b"), b"a2b");
|
||||||
|
assert_eq!(escape(b"a3b"), b"a3b");
|
||||||
|
assert_eq!(escape(b"a4b"), b"a4b");
|
||||||
|
assert_eq!(escape(b"a5b"), b"a5b");
|
||||||
|
assert_eq!(escape(b"a6b"), b"a6b");
|
||||||
|
assert_eq!(escape(b"a7b"), b"a7b");
|
||||||
|
assert_eq!(escape(b"a8b"), b"a8b");
|
||||||
|
assert_eq!(escape(b"a9b"), b"a9b");
|
||||||
|
|
||||||
|
assert_eq!(escape(b"-0a"), b"-\\30 a");
|
||||||
|
assert_eq!(escape(b"-1a"), b"-\\31 a");
|
||||||
|
assert_eq!(escape(b"-2a"), b"-\\32 a");
|
||||||
|
assert_eq!(escape(b"-3a"), b"-\\33 a");
|
||||||
|
assert_eq!(escape(b"-4a"), b"-\\34 a");
|
||||||
|
assert_eq!(escape(b"-5a"), b"-\\35 a");
|
||||||
|
assert_eq!(escape(b"-6a"), b"-\\36 a");
|
||||||
|
assert_eq!(escape(b"-7a"), b"-\\37 a");
|
||||||
|
assert_eq!(escape(b"-8a"), b"-\\38 a");
|
||||||
|
assert_eq!(escape(b"-9a"), b"-\\39 a");
|
||||||
|
|
||||||
|
assert_eq!(escape(b"-"), b"\\-");
|
||||||
|
assert_eq!(escape(b"-a"), b"-a");
|
||||||
|
assert_eq!(escape(b"--"), b"--");
|
||||||
|
assert_eq!(escape(b"--a"), b"--a");
|
||||||
|
|
||||||
|
assert_eq!(escape(b"\x80\x2D\x5F\xA9"), b"\x80\x2D\x5F\xA9");
|
||||||
|
assert_eq!(escape(b"\x7F\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F"), b"\\7f \x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F");
|
||||||
|
assert_eq!(escape(b"\xA0\xA1\xA2"), b"\xA0\xA1\xA2");
|
||||||
|
assert_eq!(escape(b"a0123456789b"), b"a0123456789b");
|
||||||
|
assert_eq!(escape(b"abcdefghijklmnopqrstuvwxyz"), b"abcdefghijklmnopqrstuvwxyz");
|
||||||
|
assert_eq!(escape(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"), b"ABCDEFGHIJKLMNOPQRSTUVWXYZ");
|
||||||
|
|
||||||
|
assert_eq!(escape(b"\x20\x21\x78\x79"), b"\\ \\!xy");
|
||||||
|
|
||||||
|
// astral symbol (U+1D306 TETRAGRAM FOR CENTRE)
|
||||||
|
assert_eq!(escape("\u{1D306}".as_bytes()), "\u{1D306}".as_bytes());
|
||||||
|
|
||||||
|
// surrogates
|
||||||
|
// assert_eq!(escape("\u{D834}\u{DF06}".as_bytes()), "\u{D834}\u{DF06}".as_bytes());
|
||||||
|
// assert_eq!(escape("\u{DF06}".as_bytes()), "\u{DF06}".as_bytes());
|
||||||
|
// assert_eq!(escape("\u{D834}".as_bytes()), "\u{D834}".as_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
60
crates/core/src/util/fast_stack.rs
Normal file
60
crates/core/src/util/fast_stack.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
use super::gurantee;
|
||||||
|
|
||||||
|
pub struct FastStack {
|
||||||
|
storage: [u8; 256],
|
||||||
|
pos: usize
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FastStack {
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn new() -> FastStack {
|
||||||
|
FastStack {
|
||||||
|
storage: [0; 256],
|
||||||
|
pos: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn push(&mut self, value: u8) {
|
||||||
|
gurantee(!self.overgrown());
|
||||||
|
|
||||||
|
self.storage[self.pos] = value;
|
||||||
|
self.pos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn peek(&self) -> u8 {
|
||||||
|
gurantee(!self.overgrown());
|
||||||
|
|
||||||
|
return self.storage[self.pos - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn last(&self) -> Option<u8> {
|
||||||
|
if self.is_empty() || self.overgrown() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Some(self.peek());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn pop(&mut self) {
|
||||||
|
// SAFETY: The buffer does not need to be mutated because the stack is
|
||||||
|
// only ever read from or written to its current position. Its current
|
||||||
|
// position is only ever incremented after writing to it. Meaning that
|
||||||
|
// the buffer can be dirty for the next use and still be correct since
|
||||||
|
// reading/writing always starts at position `0`.
|
||||||
|
self.pos = self.pos.saturating_sub(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
return self.pos == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn overgrown(&self) -> bool {
|
||||||
|
self.pos > 256
|
||||||
|
}
|
||||||
|
}
|
||||||
11
crates/core/src/util/gurantee.rs
Normal file
11
crates/core/src/util/gurantee.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#[inline(always)]
|
||||||
|
pub const fn gurantee(expr: bool) {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
if !expr {
|
||||||
|
panic!("gurantee failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
std::hint::assert_unchecked(expr)
|
||||||
|
}
|
||||||
|
}
|
||||||
154
crates/core/src/util/math.rs
Normal file
154
crates/core/src/util/math.rs
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
// const mathFunctions = [
|
||||||
|
// 'calc',
|
||||||
|
// 'min',
|
||||||
|
// 'max',
|
||||||
|
// 'clamp',
|
||||||
|
// 'mod',
|
||||||
|
// 'rem',
|
||||||
|
// 'sin',
|
||||||
|
// 'cos',
|
||||||
|
// 'tan',
|
||||||
|
// 'asin',
|
||||||
|
// 'acos',
|
||||||
|
// 'atan',
|
||||||
|
// 'atan2',
|
||||||
|
// 'pow',
|
||||||
|
// 'sqrt',
|
||||||
|
// 'hypot',
|
||||||
|
// 'log',
|
||||||
|
// 'exp',
|
||||||
|
// 'round',
|
||||||
|
// ]
|
||||||
|
|
||||||
|
// export function hasMathFn(input: string) {
|
||||||
|
// return input.indexOf('(') !== -1 && mathFunctions.some((fn) => input.includes(`${fn}(`))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export function addWhitespaceAroundMathOperators(input: string) {
|
||||||
|
// // There's definitely no functions in the input, so bail early
|
||||||
|
// if (input.indexOf('(') === -1) {
|
||||||
|
// return input
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Bail early if there are no math functions in the input
|
||||||
|
// if (!mathFunctions.some((fn) => input.includes(fn))) {
|
||||||
|
// return input
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let result = ''
|
||||||
|
// let formattable: boolean[] = []
|
||||||
|
|
||||||
|
// for (let i = 0; i < input.length; i++) {
|
||||||
|
// let char = input[i]
|
||||||
|
|
||||||
|
// // Determine if we're inside a math function
|
||||||
|
// if (char === '(') {
|
||||||
|
// result += char
|
||||||
|
|
||||||
|
// // Scan backwards to determine the function name. This assumes math
|
||||||
|
// // functions are named with lowercase alphanumeric characters.
|
||||||
|
// let start = i
|
||||||
|
|
||||||
|
// for (let j = i - 1; j >= 0; j--) {
|
||||||
|
// let inner = input.charCodeAt(j)
|
||||||
|
|
||||||
|
// if (inner >= 48 && inner <= 57) {
|
||||||
|
// start = j // 0-9
|
||||||
|
// } else if (inner >= 97 && inner <= 122) {
|
||||||
|
// start = j // a-z
|
||||||
|
// } else {
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let fn = input.slice(start, i)
|
||||||
|
|
||||||
|
// // This is a known math function so start formatting
|
||||||
|
// if (mathFunctions.includes(fn)) {
|
||||||
|
// formattable.unshift(true)
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // We've encountered nested parens inside a math function, record that and
|
||||||
|
// // keep formatting until we've closed all parens.
|
||||||
|
// else if (formattable[0] && fn === '') {
|
||||||
|
// formattable.unshift(true)
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // This is not a known math function so don't format it
|
||||||
|
// formattable.unshift(false)
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // We've exited the function so format according to the parent function's
|
||||||
|
// // type.
|
||||||
|
// else if (char === ')') {
|
||||||
|
// result += char
|
||||||
|
// formattable.shift()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Add spaces after commas in math functions
|
||||||
|
// else if (char === ',' && formattable[0]) {
|
||||||
|
// result += `, `
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Skip over consecutive whitespace
|
||||||
|
// else if (char === ' ' && formattable[0] && result[result.length - 1] === ' ') {
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Add whitespace around operators inside math functions
|
||||||
|
// else if ((char === '+' || char === '*' || char === '/' || char === '-') && formattable[0]) {
|
||||||
|
// let trimmed = result.trimEnd()
|
||||||
|
// let prev = trimmed[trimmed.length - 1]
|
||||||
|
|
||||||
|
// // If we're preceded by an operator don't add spaces
|
||||||
|
// if (prev === '+' || prev === '*' || prev === '/' || prev === '-') {
|
||||||
|
// result += char
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // If we're at the beginning of an argument don't add spaces
|
||||||
|
// else if (prev === '(' || prev === ',') {
|
||||||
|
// result += char
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Add spaces only after the operator if we already have spaces before it
|
||||||
|
// else if (input[i - 1] === ' ') {
|
||||||
|
// result += `${char} `
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Add spaces around the operator
|
||||||
|
// else {
|
||||||
|
// result += ` ${char} `
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Skip over `to-zero` when in a math function.
|
||||||
|
// //
|
||||||
|
// // This is specifically to handle this value in the round(…) function:
|
||||||
|
// //
|
||||||
|
// // ```
|
||||||
|
// // round(to-zero, 1px)
|
||||||
|
// // ^^^^^^^
|
||||||
|
// // ```
|
||||||
|
// //
|
||||||
|
// // This is because the first argument is optionally a keyword and `to-zero`
|
||||||
|
// // contains a hyphen and we want to avoid adding spaces inside it.
|
||||||
|
// else if (formattable[0] && input.startsWith('to-zero', i)) {
|
||||||
|
// let start = i
|
||||||
|
// i += 7
|
||||||
|
// result += input.slice(start, i + 1)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Handle all other characters
|
||||||
|
// else {
|
||||||
|
// result += char
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return result
|
||||||
|
// }
|
||||||
28
crates/core/src/util/mod.rs
Normal file
28
crates/core/src/util/mod.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
mod decode;
|
||||||
|
mod escape;
|
||||||
|
mod fast_stack;
|
||||||
|
mod gurantee;
|
||||||
|
mod math;
|
||||||
|
mod segment;
|
||||||
|
mod throughput;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub use crate::util::decode::*;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub use crate::util::math::*;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub use crate::util::escape::*;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub use crate::util::fast_stack::*;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub use crate::util::gurantee::*;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub use crate::util::segment::*;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub use crate::util::throughput::*;
|
||||||
241
crates/core/src/util/segment.rs
Normal file
241
crates/core/src/util/segment.rs
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
use crate::util::gurantee::gurantee;
|
||||||
|
|
||||||
|
use super::fast_stack::FastStack;
|
||||||
|
|
||||||
|
// The order of these cases is important for performance
|
||||||
|
// please do not change it without significant profiling
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
enum Case {
|
||||||
|
Other,
|
||||||
|
|
||||||
|
// This makes things faster
|
||||||
|
// It has to be the 2nd case
|
||||||
|
Dummy1,
|
||||||
|
|
||||||
|
Close, // b')'
|
||||||
|
ParenL, // b'('
|
||||||
|
BracketL, // b'['
|
||||||
|
CurlyL, // b'{'
|
||||||
|
|
||||||
|
Escape, // b'\'
|
||||||
|
QuoteDouble, // b'"'
|
||||||
|
QuoteSingle, // b'\''
|
||||||
|
Separator, // other
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn generate_cases(separator: u8) -> [Case; 256] {
|
||||||
|
let mut table = [Case::Other; 256];
|
||||||
|
|
||||||
|
table[b'\\' as usize] = Case::Escape;
|
||||||
|
table[b'"' as usize] = Case::QuoteDouble;
|
||||||
|
table[b'\'' as usize] = Case::QuoteSingle;
|
||||||
|
table[b'(' as usize] = Case::ParenL;
|
||||||
|
table[b'[' as usize] = Case::BracketL;
|
||||||
|
table[b'{' as usize] = Case::CurlyL;
|
||||||
|
table[b')' as usize] = Case::Close;
|
||||||
|
table[b']' as usize] = Case::Close;
|
||||||
|
table[b'}' as usize] = Case::Close;
|
||||||
|
table[separator as usize] = Case::Separator;
|
||||||
|
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This splits a string on a top-level character.
|
||||||
|
*
|
||||||
|
* Regex doesn't support recursion (at least not the JS-flavored version),
|
||||||
|
* so we have to use a tiny state machine to keep track of paren placement.
|
||||||
|
*
|
||||||
|
* Expected behavior using commas:
|
||||||
|
* var(--a, 0 0 1px rgb(0, 0, 0)), 0 0 1px rgb(0, 0, 0)
|
||||||
|
* ┬ ┬ ┬ ┬
|
||||||
|
* x x x ╰──────── Split because top-level
|
||||||
|
* ╰──────────────┴──┴───────────── Ignored b/c inside >= 1 levels of parens
|
||||||
|
*/
|
||||||
|
#[inline(never)]
|
||||||
|
#[no_mangle]
|
||||||
|
pub fn segment(input: &[u8], separator: u8) -> Vec<&[u8]> {
|
||||||
|
return segment_table(input, generate_cases(separator))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn segment_table(input: &[u8], cases: [Case; 256]) -> Vec<&[u8]> {
|
||||||
|
let mut closing_bracket_stack = FastStack::new();
|
||||||
|
|
||||||
|
let mut parts: Vec<&[u8]> = vec![];
|
||||||
|
let mut last_pos = 0;
|
||||||
|
|
||||||
|
let mut idx = 0;
|
||||||
|
while idx < input.len() {
|
||||||
|
// SAFETY: last_pos can never be greater than idx at the start of the loop
|
||||||
|
// body. It's set to one greater than idx on a separator but idx is always
|
||||||
|
// incremented before the next iteration.
|
||||||
|
gurantee(last_pos <= idx);
|
||||||
|
|
||||||
|
match cases[input[idx] as usize] {
|
||||||
|
Case::Separator => {
|
||||||
|
if closing_bracket_stack.is_empty() {
|
||||||
|
parts.push(&input[last_pos..idx]);
|
||||||
|
last_pos = idx + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The next character is escaped, so we skip it.
|
||||||
|
Case::Escape => idx += 1,
|
||||||
|
|
||||||
|
// Strings should be handled as-is until the end of the string. No need to
|
||||||
|
// worry about balancing parens, brackets, or curlies inside a string.
|
||||||
|
Case::QuoteDouble => {
|
||||||
|
loop {
|
||||||
|
idx += 1;
|
||||||
|
|
||||||
|
// Ensure we don't go out of bounds.
|
||||||
|
if idx >= input.len() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
match cases[input[idx] as usize] {
|
||||||
|
Case::Escape => idx += 1,
|
||||||
|
Case::QuoteDouble => break,
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strings should be handled as-is until the end of the string. No need to
|
||||||
|
// worry about balancing parens, brackets, or curlies inside a string.
|
||||||
|
Case::QuoteSingle => {
|
||||||
|
loop {
|
||||||
|
idx += 1;
|
||||||
|
|
||||||
|
// Ensure we don't go out of bounds.
|
||||||
|
if idx >= input.len() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
match cases[input[idx] as usize] {
|
||||||
|
Case::Escape => idx += 1,
|
||||||
|
Case::QuoteSingle => break,
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Case::ParenL => {
|
||||||
|
closing_bracket_stack.push(b')');
|
||||||
|
|
||||||
|
if closing_bracket_stack.overgrown() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Case::BracketL => {
|
||||||
|
closing_bracket_stack.push(b']');
|
||||||
|
|
||||||
|
if closing_bracket_stack.overgrown() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Case::CurlyL => {
|
||||||
|
closing_bracket_stack.push(b'}');
|
||||||
|
|
||||||
|
if closing_bracket_stack.overgrown() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Case::Close => {
|
||||||
|
if !closing_bracket_stack.is_empty() && closing_bracket_stack.peek() == input[idx] {
|
||||||
|
closing_bracket_stack.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Case::Other => {},
|
||||||
|
Case::Dummy1 => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: last_pos will be at most `input.len() - 1` ensuring that the slice
|
||||||
|
// is always within bounds.
|
||||||
|
gurantee(last_pos < input.len());
|
||||||
|
|
||||||
|
parts.push(&input[last_pos..]);
|
||||||
|
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_result_in_a_single_segment_when_the_separator_is_not_present() {
|
||||||
|
assert_eq!(segment(b"foo", b':'), vec![b"foo"])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_split_by_the_separator() {
|
||||||
|
assert_eq!(segment(b"foo:bar:baz", b':'), vec![b"foo" as &[u8], b"bar", b"baz"])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_not_split_inside_of_parens() {
|
||||||
|
assert_eq!(segment(b"a:(b:c):d", b':'), vec![b"a" as &[u8], b"(b:c)", b"d"])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_not_split_inside_of_brackets() {
|
||||||
|
assert_eq!(segment(b"a:[b:c]:d", b':'), vec![b"a" as &[u8], b"[b:c]", b"d"])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_not_split_inside_of_curlies() {
|
||||||
|
assert_eq!(segment(b"a:{b:c}:d", b':'), vec![b"a" as &[u8], b"{b:c}", b"d"])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_not_split_inside_of_double_quotes() {
|
||||||
|
assert_eq!(segment(b"a:\"b:c\":d", b':'), vec![b"a" as &[u8], b"\"b:c\"", b"d"])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_not_split_inside_of_single_quotes() {
|
||||||
|
assert_eq!(segment(b"a:'b:c':d", b':'), vec![b"a" as &[u8], b"'b:c'", b"d"])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_not_crash_when_double_quotes_are_unbalanced() {
|
||||||
|
assert_eq!(segment(b"a:\"b:c:d", b':'), vec![b"a" as &[u8], b"\"b:c:d"])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_not_crash_when_single_quotes_are_unbalanced() {
|
||||||
|
assert_eq!(segment(b"a:'b:c:d", b':'), vec![b"a" as &[u8], b"'b:c:d"])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_skip_escaped_double_quotes() {
|
||||||
|
assert_eq!(segment(b"a:\"b:c\\\":d\":e", b':'), vec![b"a" as &[u8], b"\"b:c\\\":d\"", b"e"])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_skip_escaped_single_quotes() {
|
||||||
|
assert_eq!(segment(b"a:'b:c\\':d':e", b':'), vec![b"a" as &[u8], b"'b:c\\':d'", b"e"])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_skip_escaped_separators() {
|
||||||
|
assert_eq!(segment(b"a:b\\:c:d", b':'), vec![b"a" as &[u8], b"b\\:c", b"d"])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_split_by_the_escape_sequence_which_is_escape_as_well() {
|
||||||
|
assert_eq!(segment(b"a\\b\\c\\d", b'\\'), vec![b"a" as &[u8], b"b", b"c", b"d"]);
|
||||||
|
assert_eq!(segment(b"a\\(b\\c)\\d", b'\\'), vec![b"a" as &[u8], b"(b\\c)", b"d"]);
|
||||||
|
assert_eq!(segment(b"a\\[b\\c]\\d", b'\\'), vec![b"a" as &[u8], b"[b\\c]", b"d"]);
|
||||||
|
assert_eq!(segment(b"a\\{b\\c}\\d", b'\\'), vec![b"a" as &[u8], b"{b\\c}", b"d"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
47
crates/core/src/util/throughput.rs
Normal file
47
crates/core/src/util/throughput.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
pub struct Throughput {
|
||||||
|
rate: f64,
|
||||||
|
elapsed: std::time::Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Throughput {
|
||||||
|
#[inline(always)]
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(f, "{}/s over {:.2}s", format_byte_size(self.rate), self.elapsed.as_secs_f64())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Throughput {
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn compute<F>(iterations: usize, memory_baseline: usize, cb: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(),
|
||||||
|
{
|
||||||
|
let now = std::time::Instant::now();
|
||||||
|
for _ in 0..iterations {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
let elapsed = now.elapsed();
|
||||||
|
let memory_size = iterations * memory_baseline;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
rate: memory_size as f64 / elapsed.as_secs_f64(),
|
||||||
|
elapsed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn format_byte_size(size: f64) -> String {
|
||||||
|
let units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||||
|
let unit = 1000;
|
||||||
|
let mut size = size;
|
||||||
|
let mut i = 0;
|
||||||
|
while size > unit as f64 {
|
||||||
|
size /= unit as f64;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
format!("{:.2} {}", size, units[i])
|
||||||
|
}
|
||||||
|
|
@ -2,10 +2,10 @@ use std::{ascii::escape_default, fmt::Display};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Cursor<'a> {
|
pub struct Cursor<'a> {
|
||||||
// The input we're scanning
|
/// The input we're scanning
|
||||||
pub input: &'a [u8],
|
pub input: &'a [u8],
|
||||||
|
|
||||||
// The location of the cursor in the input
|
/// The location of the cursor in the input
|
||||||
pub pos: usize,
|
pub pos: usize,
|
||||||
|
|
||||||
/// Is the cursor at the start of the input
|
/// Is the cursor at the start of the input
|
||||||
|
|
|
||||||
116
crates/oxide/src/extractor.rs
Normal file
116
crates/oxide/src/extractor.rs
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
/// Tailwind CSS Candidate Extractor
|
||||||
|
///
|
||||||
|
/// Core assumptions:
|
||||||
|
/// - The extractor is intended to scan data that is valid UTF-8.
|
||||||
|
/// - Scanning invalid UTF-8 may result in incorrect output.
|
||||||
|
/// - No code should **ever** panic even in the presence of invalid data.
|
||||||
|
///
|
||||||
|
/// The extractor is designed to operate on a tuple of two pieces of data:
|
||||||
|
/// - The current state
|
||||||
|
/// - The current byte
|
||||||
|
///
|
||||||
|
/// A byte-to-fn-pointer table is used per-state such that the CPU can
|
||||||
|
/// accurately predict upcoming branches. This is a critical optimization
|
||||||
|
/// that allows the extractor to run at maximum speed without requiring
|
||||||
|
/// SIMD-like parallelism for every operation.
|
||||||
|
///
|
||||||
|
|
||||||
|
/// This represents the current "state" of the extractor.
|
||||||
|
enum ParseState {
|
||||||
|
Start,
|
||||||
|
Candidate,
|
||||||
|
Arbitrary,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Enter arbitrary value mode
|
||||||
|
// b'[' => {
|
||||||
|
// trace!("Arbitrary::Start\t");
|
||||||
|
// self.in_arbitrary = true;
|
||||||
|
// self.idx_arbitrary_start = self.cursor.pos;
|
||||||
|
|
||||||
|
// ParseAction::Consume
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Allowed first characters.
|
||||||
|
// b'@' | b'!' | b'-' | b'<' | b'>' | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'*' => {
|
||||||
|
// // TODO: A bunch of characters that we currently support but maybe we only want it behind
|
||||||
|
// // a flag. E.g.: `<sm`
|
||||||
|
// // | '$' | '^' | '_'
|
||||||
|
|
||||||
|
// // When the new candidate is preceded by a `:`, then we want to keep parsing, but
|
||||||
|
// // throw away the full candidate because it can not be a valid candidate at the end
|
||||||
|
// // of the day.
|
||||||
|
// if self.cursor.prev == b':' {
|
||||||
|
// self.discard_next = true;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// trace!("Candidate::Start\t");
|
||||||
|
|
||||||
|
// ParseAction::Consume
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const TABLE_START: [TableCaseStart; 256] = {
|
||||||
|
// //
|
||||||
|
// };
|
||||||
|
|
||||||
|
// static __CASES: [Case; 256] = {
|
||||||
|
// let mut cases = [Case::Other; 256];
|
||||||
|
|
||||||
|
// cases[0x00] = Case::Nul;
|
||||||
|
|
||||||
|
// let mut i = 0x01;
|
||||||
|
// while i <= 0x1f {
|
||||||
|
// cases[i] = Case::Control;
|
||||||
|
// i+=1;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// cases[0x7f] = Case::Control;
|
||||||
|
|
||||||
|
// let mut i = b'0';
|
||||||
|
// while i <= b'9' {
|
||||||
|
// cases[i as usize] = Case::Ident;
|
||||||
|
// i+=1;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let mut i = b'A';
|
||||||
|
// while i <= b'Z' {
|
||||||
|
// cases[i as usize] = Case::Ident;
|
||||||
|
// i+=1;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let mut i = b'a';
|
||||||
|
// while i <= b'z' {
|
||||||
|
// cases[i as usize] = Case::Ident;
|
||||||
|
// i+=1;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// cases[b'-' as usize] = Case::Ident;
|
||||||
|
// cases[b'_' as usize] = Case::Ident;
|
||||||
|
|
||||||
|
// let mut i = 0x80;
|
||||||
|
// while i <= 0xff {
|
||||||
|
// cases[i] = Case::Ident;
|
||||||
|
// i+=1;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// cases
|
||||||
|
// };
|
||||||
|
|
||||||
|
|
||||||
|
// #[inline(always)]
|
||||||
|
// fn parse_char(&mut self) -> ParseAction<'a> {
|
||||||
|
// if self.in_arbitrary {
|
||||||
|
// self.parse_arbitrary()
|
||||||
|
// } else if self.in_candidate {
|
||||||
|
// self.parse_continue()
|
||||||
|
// } else if self.parse_start() == ParseAction::Consume {
|
||||||
|
// self.in_candidate = true;
|
||||||
|
// self.idx_start = self.cursor.pos;
|
||||||
|
// self.idx_end = self.cursor.pos;
|
||||||
|
|
||||||
|
// ParseAction::Consume
|
||||||
|
// } else {
|
||||||
|
// ParseAction::Skip
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
@ -80,10 +80,10 @@ fn is_ascii_whitespace(value: [u8; STRIDE]) -> [bool; STRIDE] {
|
||||||
let whitespace_5 = eq(value, b' ');
|
let whitespace_5 = eq(value, b' ');
|
||||||
|
|
||||||
or(
|
or(
|
||||||
or(
|
or(
|
||||||
or(or(whitespace_1, whitespace_2), whitespace_3),
|
or(whitespace_1, whitespace_2),
|
||||||
whitespace_4,
|
or(whitespace_3, whitespace_4)
|
||||||
),
|
),
|
||||||
whitespace_5,
|
whitespace_5
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ pub mod fast_skip;
|
||||||
pub mod glob;
|
pub mod glob;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
pub mod scanner;
|
pub mod scanner;
|
||||||
|
pub mod extractor;
|
||||||
|
|
||||||
static SHOULD_TRACE: sync::LazyLock<bool> = sync::LazyLock::new(
|
static SHOULD_TRACE: sync::LazyLock<bool> = sync::LazyLock::new(
|
||||||
|| matches!(std::env::var("DEBUG"), Ok(value) if value.eq("*") || value.eq("1") || value.eq("true") || value.contains("tailwind")),
|
|| matches!(std::env::var("DEBUG"), Ok(value) if value.eq("*") || value.eq("1") || value.eq("true") || value.contains("tailwind")),
|
||||||
|
|
|
||||||
15
crates/plugin-example/Cargo.toml
Normal file
15
crates/plugin-example/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "tailwindcss-plugin-example"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
lsp-server = "0.7.6"
|
||||||
|
serde = "1.0.209"
|
||||||
|
serde_json = "1.0.127"
|
||||||
|
tracing = "0.1.40"
|
||||||
|
tailwindcss-core = { path = "../core" }
|
||||||
|
crossbeam-channel = "0.5.13"
|
||||||
|
log = "0.4.22"
|
||||||
|
tokio = { version = "1.39.3", features = ["full"] }
|
||||||
|
mio = "1.0.2"
|
||||||
58
crates/plugin-example/node/package-lock.json
generated
Normal file
58
crates/plugin-example/node/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
{
|
||||||
|
"name": "node",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "node",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"vscode-jsonrpc": "^8.2.1",
|
||||||
|
"vscode-languageserver": "^9.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vscode-jsonrpc": {
|
||||||
|
"version": "8.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz",
|
||||||
|
"integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vscode-languageserver": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==",
|
||||||
|
"dependencies": {
|
||||||
|
"vscode-languageserver-protocol": "3.17.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"installServerIntoExtension": "bin/installServerIntoExtension"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vscode-languageserver-protocol": {
|
||||||
|
"version": "3.17.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
|
||||||
|
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
|
||||||
|
"dependencies": {
|
||||||
|
"vscode-jsonrpc": "8.2.0",
|
||||||
|
"vscode-languageserver-types": "3.17.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vscode-languageserver-protocol/node_modules/vscode-jsonrpc": {
|
||||||
|
"version": "8.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
|
||||||
|
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vscode-languageserver-types": {
|
||||||
|
"version": "3.17.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
|
||||||
|
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
crates/plugin-example/node/package.json
Normal file
15
crates/plugin-example/node/package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "node",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"vscode-jsonrpc": "^8.2.1",
|
||||||
|
"vscode-languageserver": "^9.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
crates/plugin-example/node/src/client.ts
Normal file
14
crates/plugin-example/node/src/client.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import * as net from 'node:net'
|
||||||
|
import * as rpc from 'vscode-jsonrpc/node'
|
||||||
|
|
||||||
|
let client = net.connect(12345)
|
||||||
|
|
||||||
|
// Use stdin and stdout for communication:
|
||||||
|
let connection = rpc.createMessageConnection(client, client)
|
||||||
|
|
||||||
|
connection.onNotification('loaded', () => console.log('server loaded'))
|
||||||
|
connection.onNotification('ping', () => console.log('ping'))
|
||||||
|
connection.onNotification('@/plugins/loaded', () => console.log('plugins loaded'))
|
||||||
|
|
||||||
|
connection.listen()
|
||||||
|
connection.sendRequest('@/plugins/load', 'foo')
|
||||||
127
crates/plugin-example/node/src/plugin-api.ts
Normal file
127
crates/plugin-example/node/src/plugin-api.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
export type PluginFn = (api: PluginAPI) => void
|
||||||
|
export type PluginWithConfig = { handler: PluginFn }
|
||||||
|
export type PluginWithOptions<T> = {
|
||||||
|
(options?: T): PluginWithConfig
|
||||||
|
__isOptionsFunction: true
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Plugin = PluginFn | PluginWithConfig | PluginWithOptions<any>
|
||||||
|
export type CssInJs = { [key: string]: string | CssInJs | CssInJs[] }
|
||||||
|
|
||||||
|
export type NamedUtilityValue = {
|
||||||
|
kind: 'named'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* bg-red-500
|
||||||
|
* ^^^^^^^
|
||||||
|
*
|
||||||
|
* w-1/2
|
||||||
|
* ^
|
||||||
|
*/
|
||||||
|
value: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* w-1/2
|
||||||
|
* ^^^
|
||||||
|
*/
|
||||||
|
fraction: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginAPI = {
|
||||||
|
addBase(base: CssInJs): void
|
||||||
|
addVariant(name: string, variant: string | string[] | CssInJs): void
|
||||||
|
|
||||||
|
addUtilities(
|
||||||
|
utilities: Record<string, CssInJs | CssInJs[]> | Record<string, CssInJs | CssInJs[]>[],
|
||||||
|
options?: {},
|
||||||
|
): void
|
||||||
|
|
||||||
|
matchUtilities(
|
||||||
|
utilities: Record<
|
||||||
|
string,
|
||||||
|
(value: string, extra: { modifier: string | null }) => CssInJs | CssInJs[]
|
||||||
|
>,
|
||||||
|
options?: Partial<{
|
||||||
|
type: string | string[]
|
||||||
|
supportsNegativeValues: boolean
|
||||||
|
values: Record<string, string> & {
|
||||||
|
__BARE_VALUE__?: (value: NamedUtilityValue) => string | undefined
|
||||||
|
}
|
||||||
|
modifiers: 'any' | Record<string, string>
|
||||||
|
}>,
|
||||||
|
): void
|
||||||
|
|
||||||
|
// addComponents(utilities: Record<string, CssInJs> | Record<string, CssInJs>[], options?: {}): void
|
||||||
|
// matchComponents(
|
||||||
|
// utilities: Record<string, (value: string, extra: { modifier: string | null }) => CssInJs>,
|
||||||
|
// options?: Partial<{
|
||||||
|
// type: string | string[]
|
||||||
|
// supportsNegativeValues: boolean
|
||||||
|
// values: Record<string, string> & {
|
||||||
|
// __BARE_VALUE__?: (value: NamedUtilityValue) => string | undefined
|
||||||
|
// }
|
||||||
|
// modifiers: 'any' | Record<string, string>
|
||||||
|
// }>,
|
||||||
|
// ): void
|
||||||
|
|
||||||
|
// theme(path: string, defaultValue?: any): any
|
||||||
|
// prefix(className: string): string
|
||||||
|
}
|
||||||
|
|
||||||
|
import * as rpc from 'vscode-jsonrpc/node'
|
||||||
|
|
||||||
|
export function buildPluginController(server: rpc.MessageConnection) {
|
||||||
|
let utilityMap = new Map<
|
||||||
|
string,
|
||||||
|
(value: string, extra: { modifier: string | null }) => CssInJs | CssInJs[]
|
||||||
|
>()
|
||||||
|
|
||||||
|
let api = buildPluginApi(server, utilityMap)
|
||||||
|
|
||||||
|
return {
|
||||||
|
api,
|
||||||
|
|
||||||
|
matchUtility(id: string, value: string, modifier: string | null) {
|
||||||
|
let fn = utilityMap.get(id)
|
||||||
|
if (!fn) return
|
||||||
|
|
||||||
|
return fn(value, { modifier })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPluginApi(
|
||||||
|
server: rpc.MessageConnection,
|
||||||
|
utilityMap: Map<
|
||||||
|
string,
|
||||||
|
(value: string, extra: { modifier: string | null }) => CssInJs | CssInJs[]
|
||||||
|
>,
|
||||||
|
): PluginAPI {
|
||||||
|
return {
|
||||||
|
addBase(base) {
|
||||||
|
server.sendNotification('@/plugin/add-base', { ast: base })
|
||||||
|
},
|
||||||
|
|
||||||
|
addVariant(name, variant) {
|
||||||
|
server.sendNotification('@/plugin/add-variant', { name, format: variant })
|
||||||
|
},
|
||||||
|
|
||||||
|
addUtilities(utilities, options) {
|
||||||
|
server.sendNotification('@/plugin/add-utilities', { ast: utilities, options })
|
||||||
|
},
|
||||||
|
|
||||||
|
matchUtilities(utilities, options) {
|
||||||
|
let namesToIds: Record<string, string> = {}
|
||||||
|
|
||||||
|
for (let [name, fn] of Object.entries(utilities)) {
|
||||||
|
let id = Math.random().toString(36).slice(2)
|
||||||
|
namesToIds[name] = id
|
||||||
|
utilityMap.set(id, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
server.sendNotification('@/plugin/match-utility', {
|
||||||
|
utilities: namesToIds,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
27
crates/plugin-example/node/src/plugin.ts
Normal file
27
crates/plugin-example/node/src/plugin.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import type { PluginAPI } from './plugin-api'
|
||||||
|
|
||||||
|
module.exports = function ({ addBase, addVariant, addUtilities, matchUtilities }: PluginAPI) {
|
||||||
|
addBase({
|
||||||
|
div: {
|
||||||
|
transform: 'skewY(-10deg)',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
addVariant('hover', ':hover')
|
||||||
|
addVariant('marker', ['::marker', '* ::marker'])
|
||||||
|
|
||||||
|
addUtilities({
|
||||||
|
'.skew-10deg': {
|
||||||
|
transform: 'skewY(-10deg)',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
matchUtilities(
|
||||||
|
{ skew: (value) => ({ transform: `skewY(${value})` }) },
|
||||||
|
{
|
||||||
|
values: {
|
||||||
|
f10: '10deg',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
33
crates/plugin-example/node/src/server.ts
Normal file
33
crates/plugin-example/node/src/server.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import * as net from 'node:net'
|
||||||
|
import * as rpc from 'vscode-jsonrpc/node'
|
||||||
|
import { buildPluginController, PluginFn } from './plugin-api'
|
||||||
|
|
||||||
|
let socket = await new Promise<net.Socket>((resolve) => {
|
||||||
|
net.createServer(resolve).listen(12345)
|
||||||
|
})
|
||||||
|
|
||||||
|
let server = rpc.createMessageConnection(socket, socket)
|
||||||
|
let controller = buildPluginController(server)
|
||||||
|
|
||||||
|
async function loadPlugin(path: string): Promise<PluginFn> {
|
||||||
|
return import(path).then((m) => m.default ?? m)
|
||||||
|
}
|
||||||
|
|
||||||
|
server.onRequest('@/plugins/load', async ({ plugins: paths }) => {
|
||||||
|
console.log('Loading…')
|
||||||
|
let plugins: PluginFn[] = await Promise.all(paths.map(loadPlugin))
|
||||||
|
|
||||||
|
for (let plugin of plugins) {
|
||||||
|
plugin(controller.api)
|
||||||
|
}
|
||||||
|
console.log('Loaded…')
|
||||||
|
})
|
||||||
|
|
||||||
|
server.onRequest('@/plugins/match-utility', async ({ id, value, modifier }) => {
|
||||||
|
let ast = controller.matchUtility(id, value, modifier)
|
||||||
|
|
||||||
|
return { ast }
|
||||||
|
})
|
||||||
|
|
||||||
|
server.listen()
|
||||||
|
server.sendNotification('loaded')
|
||||||
111
crates/plugin-example/src/app.rs
Normal file
111
crates/plugin-example/src/app.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
use std::{collections::HashMap, error::Error};
|
||||||
|
|
||||||
|
use lsp_server::{Connection, Message, Notification, ReqQueue, Request, RequestId, Response};
|
||||||
|
use serde_json::Value;
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
|
type AppResult<T> = Result<T, Box<dyn Error + Sync + Send>>;
|
||||||
|
type ReqHandler = Box<dyn Fn(&mut App, Response) -> ()>;
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
connection: Connection,
|
||||||
|
queue: ReqQueue::<usize, ReqHandler>,
|
||||||
|
utilities: HashMap::<String, Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new(connection: Connection) -> Self {
|
||||||
|
Self {
|
||||||
|
connection,
|
||||||
|
queue: ReqQueue::default(),
|
||||||
|
utilities: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn main_loop(&mut self) -> AppResult<()> {
|
||||||
|
for msg in self.connection.receiver.clone() {
|
||||||
|
match msg {
|
||||||
|
Message::Request(req) => self.on_request(req).await?,
|
||||||
|
Message::Response(res) => self.on_response(res).await?,
|
||||||
|
Message::Notification(note) => self.on_note(note).await?,
|
||||||
|
_ => {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_request(&mut self, req: Request) -> AppResult<()> {
|
||||||
|
//
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_response(&mut self, res: Response) -> AppResult<()> {
|
||||||
|
self.queue.outgoing.complete(res.id);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_note(&mut self, note: Notification) -> AppResult<()> {
|
||||||
|
match ¬e.method[..] {
|
||||||
|
"@/plugin/match-utility" => {
|
||||||
|
self.on_match_utility(note.params)?;
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_plugins(&mut self, paths: Vec<String>) -> AppResult<()> {
|
||||||
|
let (tx, rx) = oneshot::channel::<usize>();
|
||||||
|
|
||||||
|
let request = self.queue.outgoing.register(
|
||||||
|
"@/plugins/load".to_owned(),
|
||||||
|
serde_json::json!({
|
||||||
|
"plugins": paths,
|
||||||
|
}),
|
||||||
|
Box::new(move |app, res| {
|
||||||
|
tx.send(0).unwrap();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let id = request.id.clone();
|
||||||
|
|
||||||
|
self.connection.sender.send(Message::Request(request))?;
|
||||||
|
|
||||||
|
rx.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_match_utility(&mut self, params: Value) -> AppResult<()> {
|
||||||
|
let params = params["utilities"].as_object().unwrap();
|
||||||
|
|
||||||
|
for (key, value) in params {
|
||||||
|
let id = value.as_str().unwrap().to_owned();
|
||||||
|
|
||||||
|
self.utilities.entry(key.clone()).or_default().push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn call_match_utility(&mut self, name: &str, value: &str) -> AppResult<()> {
|
||||||
|
let utility_id = self.utilities.get(name).unwrap().first().unwrap().clone();
|
||||||
|
|
||||||
|
let request = self.queue.outgoing.register(
|
||||||
|
"@/plugins/match-utility".to_owned(),
|
||||||
|
serde_json::json!({
|
||||||
|
"id": utility_id,
|
||||||
|
"value": value,
|
||||||
|
"modifier": null,
|
||||||
|
}),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
self.connection.sender.send(Message::Request(request))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
73
crates/plugin-example/src/main.rs
Normal file
73
crates/plugin-example/src/main.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
mod app;
|
||||||
|
mod pipes;
|
||||||
|
|
||||||
|
use std::{collections::HashMap, error::Error, net::SocketAddr, process::{Command, Stdio}, thread, time::Duration};
|
||||||
|
|
||||||
|
use app::App;
|
||||||
|
use lsp_server::{Connection, Message, ReqQueue, Request};
|
||||||
|
use tailwindcss_css::ast::{rule, AstNode, WalkAction};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error + Sync + Send>> {
|
||||||
|
let mut ast: Vec<AstNode> = vec![
|
||||||
|
rule(b"@plugin \"./plugin.ts\"".to_vec(), vec![]),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 1. Collect paths of all plugins in the CSS
|
||||||
|
let mut plugins_paths: Vec<String> = vec![];
|
||||||
|
|
||||||
|
for node in ast.iter_mut() {
|
||||||
|
node.walk(&mut |node, _| {
|
||||||
|
let AstNode::Rule { selector, .. } = node else {
|
||||||
|
return WalkAction::Continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !selector.starts_with(b"@plugin") {
|
||||||
|
return WalkAction::Continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = selector.split_at(7).1;
|
||||||
|
let path = &path[2..path.len()-1];
|
||||||
|
let path = path.to_vec();
|
||||||
|
|
||||||
|
plugins_paths.push(unsafe {
|
||||||
|
String::from_utf8_unchecked(path)
|
||||||
|
});
|
||||||
|
|
||||||
|
WalkAction::Continue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Spawn node server
|
||||||
|
let mut node_server = Command::new("bun")
|
||||||
|
.args(["node/src/server.ts"])
|
||||||
|
.stdin(Stdio::inherit())
|
||||||
|
.stdout(Stdio::inherit())
|
||||||
|
.spawn()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// 3. Wait for the server to start
|
||||||
|
thread::sleep(Duration::from_millis(500));
|
||||||
|
|
||||||
|
let (conn, io_threads) = Connection::connect("127.0.0.1:12345".parse::<SocketAddr>()?)?;
|
||||||
|
|
||||||
|
let mut app = App::new(conn);
|
||||||
|
|
||||||
|
_ = app.main_loop().await;
|
||||||
|
|
||||||
|
// 4. Load Plugins
|
||||||
|
app.load_plugins(plugins_paths).await?;
|
||||||
|
|
||||||
|
// 6. Ask server to match utilities
|
||||||
|
app.call_match_utility("skew", "f10").await?;
|
||||||
|
|
||||||
|
// 7. Wait for the server to respond
|
||||||
|
thread::sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
|
// 7. Update the AST with the matched utilities
|
||||||
|
|
||||||
|
io_threads.join()?;
|
||||||
|
node_server.kill()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
79
crates/plugin-example/src/pipes.rs
Normal file
79
crates/plugin-example/src/pipes.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
use std::{
|
||||||
|
io::{self, stdin, stdout, BufReader, BufWriter, Write}, process::{Child, ChildStdin, ChildStdout}, thread
|
||||||
|
};
|
||||||
|
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
|
use crossbeam_channel::{bounded, Receiver, Sender};
|
||||||
|
use lsp_server::Message;
|
||||||
|
|
||||||
|
/// Creates an LSP connection via stdio.
|
||||||
|
pub fn process_transport(
|
||||||
|
child: &mut Child,
|
||||||
|
) -> (Sender<Message>, Receiver<Message>, IoThreads) {
|
||||||
|
let stdout = child.stdin.take().expect("failed to get child stdin");
|
||||||
|
let stdin = child.stdout.take().expect("failed to get child stdout");
|
||||||
|
|
||||||
|
pipes_transport(stdout, stdin)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an LSP connection via stdio.
|
||||||
|
pub fn pipes_transport(
|
||||||
|
stdout: ChildStdin,
|
||||||
|
stdin: ChildStdout,
|
||||||
|
) -> (Sender<Message>, Receiver<Message>, IoThreads) {
|
||||||
|
// Send messages to the child process
|
||||||
|
let mut writer = stdout;
|
||||||
|
let (writer_sender, writer_receiver) = bounded::<Message>(0);
|
||||||
|
let writer = thread::spawn(move || {
|
||||||
|
writer_receiver.into_iter().try_for_each(|it| it.write(&mut writer))
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recieve messages from the child process
|
||||||
|
let mut reader = BufReader::with_capacity(1, stdin);
|
||||||
|
let (reader_sender, reader_receiver) = bounded::<Message>(0);
|
||||||
|
let reader = thread::spawn(move || {
|
||||||
|
loop {
|
||||||
|
let Some(msg) = Message::read(&mut reader)? else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_exit = matches!(&msg, Message::Notification(n) if n.method == "exit");
|
||||||
|
|
||||||
|
debug!("sending message {:#?}", msg);
|
||||||
|
reader_sender.send(msg).expect("receiver was dropped, failed to send a message");
|
||||||
|
|
||||||
|
if is_exit {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
let threads = IoThreads { reader, writer };
|
||||||
|
(writer_sender, reader_receiver, threads)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct IoThreads {
|
||||||
|
reader: thread::JoinHandle<io::Result<()>>,
|
||||||
|
writer: thread::JoinHandle<io::Result<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IoThreads {
|
||||||
|
pub fn join(self) -> io::Result<()> {
|
||||||
|
match self.reader.join() {
|
||||||
|
Ok(r) => r?,
|
||||||
|
Err(err) => {
|
||||||
|
println!("reader panicked!");
|
||||||
|
std::panic::panic_any(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match self.writer.join() {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(err) => {
|
||||||
|
println!("writer panicked!");
|
||||||
|
std::panic::panic_any(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import * as CSS from './css-parser'
|
import * as CSS from './css-parser'
|
||||||
|
import { segment } from './utils/segment'
|
||||||
|
|
||||||
const css = String.raw
|
const css = String.raw
|
||||||
|
|
||||||
|
|
@ -1036,3 +1037,49 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it.only('wip css-parser on preflight.css', async () => {
|
||||||
|
const { fileURLToPath } = await import('node:url')
|
||||||
|
const { readFile } = await import('node:fs/promises')
|
||||||
|
const currentFolder = fileURLToPath(new URL('..', import.meta.url))
|
||||||
|
const cssFile = await readFile(currentFolder + './preflight.css', 'utf-8')
|
||||||
|
|
||||||
|
function formatByteSize(size: number) {
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||||
|
const unit = 1000
|
||||||
|
let i = 0
|
||||||
|
while (size > unit) {
|
||||||
|
size /= unit
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return `${size.toFixed(2)} ${units[i]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function throughput(iterations: number, memoryBaseline: number, callback: () => void) {
|
||||||
|
let now = process.hrtime.bigint()
|
||||||
|
let len = 0
|
||||||
|
for (let i = 0; i < iterations; ++i) {
|
||||||
|
len += CSS.parse(cssFile).length
|
||||||
|
}
|
||||||
|
let elapsed = Number(process.hrtime.bigint() - now) / 1e9
|
||||||
|
let memorySize = iterations * memoryBaseline
|
||||||
|
let rate = memorySize / elapsed
|
||||||
|
|
||||||
|
return { rate, elapsed, len }
|
||||||
|
}
|
||||||
|
|
||||||
|
let t1 = throughput(100_000, cssFile.length, () => CSS.parse(cssFile).length)
|
||||||
|
console.log(`CSS.parse: ${formatByteSize(t1.rate)}/s over ${t1.elapsed}s`)
|
||||||
|
|
||||||
|
let input =
|
||||||
|
'var(--a, 0 0 1px rgb(0, 0, 0)), 0 0 1px rgb(0, 0, 0), var(--a, 0 0 1px rgb(0, 0, 0)), 0 0 1px rgb(0, 0, 0), '.repeat(
|
||||||
|
500,
|
||||||
|
)
|
||||||
|
|
||||||
|
let t2 = throughput(100_000, input.length, () => segment(input, ','))
|
||||||
|
console.log(`segment: ${formatByteSize(t2.rate)}/s over ${t2.elapsed}s`)
|
||||||
|
|
||||||
|
console.log({ t1, t2 })
|
||||||
|
|
||||||
|
throw new Error('stop here')
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.80.1"
|
channel = "1.81"
|
||||||
profile = "default"
|
profile = "default"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue