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)]
|
||||
pub struct Cursor<'a> {
|
||||
// The input we're scanning
|
||||
/// The input we're scanning
|
||||
pub input: &'a [u8],
|
||||
|
||||
// The location of the cursor in the input
|
||||
/// The location of the cursor in the input
|
||||
pub pos: usize,
|
||||
|
||||
/// 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' ');
|
||||
|
||||
or(
|
||||
or(
|
||||
or(or(whitespace_1, whitespace_2), whitespace_3),
|
||||
whitespace_4,
|
||||
),
|
||||
whitespace_5,
|
||||
or(
|
||||
or(whitespace_1, whitespace_2),
|
||||
or(whitespace_3, whitespace_4)
|
||||
),
|
||||
whitespace_5
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ pub mod fast_skip;
|
|||
pub mod glob;
|
||||
pub mod parser;
|
||||
pub mod scanner;
|
||||
pub mod extractor;
|
||||
|
||||
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")),
|
||||
|
|
|
|||
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 * as CSS from './css-parser'
|
||||
import { segment } from './utils/segment'
|
||||
|
||||
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]
|
||||
channel = "1.80.1"
|
||||
channel = "1.81"
|
||||
profile = "default"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue