Compare commits

...
Sign in to create a new pull request.

19 commits

Author SHA1 Message Date
Jordan Pittman
f1ec791afd wip: lockfile 2024-10-12 20:57:42 -04:00
Jordan Pittman
a88d3f2043 wip 2024-10-12 20:56:25 -04:00
Jordan Pittman
3f199d0745 wip: compiler 2024-10-12 20:56:05 -04:00
Jordan Pittman
86e824aeee wip: candidate
wip

wip: candidate
2024-10-12 20:56:05 -04:00
Jordan Pittman
bbcf9ecfe0 wip: testing 2024-10-12 20:56:05 -04:00
Jordan Pittman
7f19f7ef75 wip: rebuilt extractor 2024-10-12 20:56:05 -04:00
Jordan Pittman
eeb38e7399 wip: compat layer 2024-10-12 20:56:05 -04:00
Jordan Pittman
ff0a5e3f7b wip: decode 2024-10-12 20:56:05 -04:00
Jordan Pittman
8c5aec02c4 add: Throughput testing helpers 2024-10-12 20:56:05 -04:00
Jordan Pittman
25a88c57bb port: CSS.escape 2024-10-12 20:56:05 -04:00
Jordan Pittman
50bb8dbfe1 port: segment 2024-10-12 20:56:05 -04:00
Jordan Pittman
5b078a975a add: CSS AST optimizer 2024-10-12 20:56:05 -04:00
Jordan Pittman
1491289ac0 port: CSS parser 2024-10-12 20:56:05 -04:00
Jordan Pittman
17598c5c2c port: CSS serializer 2024-10-12 20:56:05 -04:00
Jordan Pittman
2dda298272 port: CSS AST 2024-10-12 20:56:05 -04:00
Jordan Pittman
191bc824c2 add: Fast Stack
This is an implementation of a stack that does minimal bookeeping, has zero heap allocations, and is guaranteed to not panic
2024-10-12 20:56:05 -04:00
Jordan Pittman
045887cd62 Cleanup 2024-10-12 20:56:05 -04:00
Jordan Pittman
7d2b791efb Reduce data dependencies in fast_skip
The CPU can compute `1 | 2` in parallel with `3 | 4`
2024-10-12 20:56:05 -04:00
Jordan Pittman
5d82f5b472 Use Rust 1.81 toolchain 2024-10-12 20:56:05 -04:00
41 changed files with 5814 additions and 125 deletions

833
Cargo.lock generated

File diff suppressed because it is too large Load diff

27
crates/core/Cargo.toml Normal file
View 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"

View file

@ -0,0 +1,5 @@
use serde_json::Value;
pub struct UserConfig {
internal: Value,
}

View file

@ -0,0 +1 @@
pub mod config;

View 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
}

View 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
View 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!();
}
}
}
}
}

View file

@ -0,0 +1,6 @@
pub mod ast;
pub mod optimize;
pub mod parser;
pub mod serializer;
pub mod syntax;
pub mod visit;

View 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);
}
}

File diff suppressed because it is too large Load diff

View 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");
}
}

View 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;
}

View 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);
}
}

View 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,
})
}

View 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
View 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
View 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);
}

View 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

View 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());
}
}

View 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
}
}

View 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)
}
}

View 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
// }

View 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::*;

View 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"]);
}
}

View 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])
}

View file

@ -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

View 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
// }
// }

View file

@ -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
)
}

View file

@ -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")),

View 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"

View 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=="
}
}
}

View 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"
}
}

View 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')

View 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,
})
},
}
}

View 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',
},
},
)
}

View 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')

View 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 &note.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(())
}
}

View 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(())
}

View 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);
}
}
}
}

View file

@ -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')
})

View file

@ -1,3 +1,3 @@
[toolchain]
channel = "1.80.1"
channel = "1.81"
profile = "default"