pub(crate) mod map;
pub(crate) mod parser;
use std::{borrow::Cow, cmp::Ordering, ops::Neg, str::FromStr};
use serde::{Deserializer, de::Error as DeError};
use crate::layout::{
Viewport,
style::{
tw::{
map::{FIXED_PROPERTIES, PREFIX_PARSERS},
parser::*,
},
*,
},
};
pub const TW_VAR_SPACING: f32 = 0.25;
#[derive(Debug, Clone, PartialEq)]
pub struct TailwindValues {
inner: Vec<TailwindValue>,
}
impl FromStr for TailwindValues {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut collected = s
.split_whitespace()
.filter_map(TailwindValue::parse)
.collect::<Vec<_>>();
collected.sort_unstable_by(|a, b| {
if !a.important && b.important {
return Ordering::Less;
}
if a.important && !b.important {
return Ordering::Greater;
}
match (&a.breakpoint, &b.breakpoint) {
(None, Some(_)) => Ordering::Less,
(Some(_), None) => Ordering::Greater,
_ => Ordering::Equal,
}
});
Ok(TailwindValues { inner: collected })
}
}
impl TailwindValues {
pub fn iter(&self) -> impl Iterator<Item = &TailwindValue> {
self.inner.iter()
}
pub(crate) fn apply(&self, style: &mut Style, viewport: Viewport) {
let mut background_image_state = TwGradientState::default();
for value in self.iter() {
value.apply(style, viewport, &mut background_image_state);
}
background_image_state.apply(style);
}
}
#[derive(Debug, Default)]
pub(crate) struct TwGradientState {
pub gradient_type: TwGradientType,
pub angle: Option<Angle>,
pub from: Option<ColorInput>,
pub to: Option<ColorInput>,
pub via: Option<ColorInput>,
}
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub(crate) enum TwGradientType {
#[default]
Linear,
Radial,
Conic,
}
impl TwGradientState {
pub(crate) fn apply(self, style: &mut Style) {
if self.from.is_none() && self.to.is_none() && self.via.is_none() && self.angle.is_none() {
return;
}
let angle = self.angle.unwrap_or_else(|| Angle::new(180.0));
let from_color = self.from.unwrap_or(ColorInput::Value(Color([0, 0, 0, 0])));
let to_color = self.to.unwrap_or_else(|| {
if let ColorInput::Value(from_c) = from_color {
ColorInput::Value(Color([from_c.0[0], from_c.0[1], from_c.0[2], 0]))
} else {
ColorInput::Value(Color([0, 0, 0, 0]))
}
});
let mut stops = Vec::new();
stops.push(GradientStop::ColorHint {
color: from_color,
hint: Some(StopPosition(Length::Percentage(0.0))),
});
if let Some(via_color) = self.via {
stops.push(GradientStop::ColorHint {
color: via_color,
hint: Some(StopPosition(Length::Percentage(50.0))),
});
}
stops.push(GradientStop::ColorHint {
color: to_color,
hint: Some(StopPosition(Length::Percentage(100.0))),
});
match self.gradient_type {
TwGradientType::Linear => {
let gradient = LinearGradient {
angle,
stops: stops.into_boxed_slice(),
};
style.background_image = [BackgroundImage::Linear(gradient)].into();
}
TwGradientType::Radial => {
let gradient = RadialGradient {
shape: RadialShape::Ellipse,
size: RadialSize::FarthestCorner,
center: BackgroundPosition::default(),
stops: stops.into_boxed_slice(),
};
style.background_image = [BackgroundImage::Radial(gradient)].into();
}
TwGradientType::Conic => {
let gradient = ConicGradient {
from_angle: angle,
center: BackgroundPosition::default(),
stops: stops.into_boxed_slice(),
};
style.background_image = [BackgroundImage::Conic(gradient)].into();
}
}
}
}
impl<'de> Deserialize<'de> for TailwindValues {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let string = String::deserialize(deserializer)?;
TailwindValues::from_str(&string).map_err(D::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TailwindValue {
pub property: TailwindProperty,
pub breakpoint: Option<Breakpoint>,
pub important: bool,
}
impl TailwindValue {
pub(crate) fn apply(
&self,
style: &mut Style,
viewport: Viewport,
gradient_state: &mut TwGradientState,
) {
if let Some(breakpoint) = self.breakpoint
&& !breakpoint.matches(viewport)
{
return;
}
self.property.apply(style, gradient_state);
}
pub fn parse(mut token: &str) -> Option<Self> {
let mut important = false;
let mut breakpoint = None;
if let Some((breakpoint_token, rest)) = token.split_once(':') {
breakpoint = Some(Breakpoint::parse(breakpoint_token)?);
token = rest;
}
if let Some(stripped) = token.strip_prefix('!') {
important = true;
token = stripped;
}
if let Some(stripped) = token.strip_suffix('!') {
important = true;
token = stripped;
}
Some(TailwindValue {
property: TailwindProperty::parse(token)?,
breakpoint,
important,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Breakpoint(pub(crate) Length);
impl Breakpoint {
pub fn parse(token: &str) -> Option<Self> {
match_ignore_ascii_case! {token,
"sm" => Some(Breakpoint(Length::Rem(40.0))),
"md" => Some(Breakpoint(Length::Rem(48.0))),
"lg" => Some(Breakpoint(Length::Rem(64.0))),
"xl" => Some(Breakpoint(Length::Rem(80.0))),
"2xl" => Some(Breakpoint(Length::Rem(96.0))),
_ => None,
}
}
pub fn matches(&self, viewport: Viewport) -> bool {
let Some(viewport_width) = viewport.width else {
return false;
};
let breakpoint_width = match self.0 {
Length::Rem(value) => value * viewport.font_size * viewport.device_pixel_ratio,
Length::Px(value) => value * viewport.device_pixel_ratio,
Length::Vw(value) => (value / 100.0) * viewport_width as f32,
_ => 0.0,
};
viewport_width >= breakpoint_width as u32
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum TailwindProperty {
BackgroundClip(BackgroundClip),
BoxSizing(BoxSizing),
FlexGrow(FlexGrow),
FlexShrink(FlexGrow),
Aspect(AspectRatio),
Items(AlignItems),
Justify(JustifyContent),
Content(JustifyContent),
JustifySelf(AlignItems),
JustifyItems(AlignItems),
AlignSelf(AlignItems),
FlexDirection(FlexDirection),
FlexWrap(FlexWrap),
Flex(Flex),
FlexBasis(Length),
Overflow(Overflow),
OverflowX(Overflow),
OverflowY(Overflow),
Position(Position),
FontStyle(FontStyle),
FontWeight(FontWeight),
FontStretch(FontStretch),
FontFamily(FontFamily),
LineClamp(LineClamp),
TextOverflow(TextOverflow),
TextWrap(TextWrap),
WhiteSpace(WhiteSpace),
WordBreak(WordBreak),
OverflowWrap(OverflowWrap),
Truncate,
TextAlign(TextAlign),
TextDecorationLine(TextDecorationLines),
TextDecorationColor(ColorInput),
TextDecorationThickness(TextDecorationThickness),
TextTransform(TextTransform),
Size(Length),
Width(Length),
Height(Length),
MinWidth(Length),
MinHeight(Length),
MaxWidth(Length),
MaxHeight(Length),
Shadow(BoxShadow),
Display(Display),
ObjectPosition(BackgroundPosition),
ObjectFit(ObjectFit),
BackgroundPosition(BackgroundPosition),
BackgroundSize(BackgroundSize),
BackgroundRepeat(BackgroundRepeat),
BackgroundImage(BackgroundImage),
Gap(Length<false>),
GapX(Length<false>),
GapY(Length<false>),
GridAutoFlow(GridAutoFlow),
GridAutoColumns(GridTrackSize),
GridAutoRows(GridTrackSize),
GridColumn(GridLine),
GridRow(GridLine),
GridColumnSpan(GridPlacementSpan),
GridRowSpan(GridPlacementSpan),
GridColumnStart(GridPlacement),
GridColumnEnd(GridPlacement),
GridRowStart(GridPlacement),
GridRowEnd(GridPlacement),
GridTemplateColumns(TwGridTemplate),
GridTemplateRows(TwGridTemplate),
LetterSpacing(TwLetterSpacing),
BorderDefault,
BorderWidth(TwBorderWidth),
BorderStyle(BorderStyle),
Color(ColorInput),
Opacity(PercentageNumber),
BackgroundColor(ColorInput<false>),
BorderColor(ColorInput),
BorderTopWidth(TwBorderWidth),
BorderRightWidth(TwBorderWidth),
BorderBottomWidth(TwBorderWidth),
BorderLeftWidth(TwBorderWidth),
BorderXWidth(TwBorderWidth),
BorderYWidth(TwBorderWidth),
OutlineDefault,
OutlineWidth(TwBorderWidth),
OutlineColor(ColorInput),
OutlineStyle(BorderStyle),
OutlineOffset(TwBorderWidth),
Rounded(TwRounded),
RoundedTopLeft(TwRounded),
RoundedTopRight(TwRounded),
RoundedBottomRight(TwRounded),
RoundedBottomLeft(TwRounded),
RoundedTop(TwRounded),
RoundedRight(TwRounded),
RoundedBottom(TwRounded),
RoundedLeft(TwRounded),
FontSize(TwFontSize),
LineHeight(LineHeight),
Translate(Length),
TranslateX(Length),
TranslateY(Length),
Rotate(Angle),
Scale(PercentageNumber),
ScaleX(PercentageNumber),
ScaleY(PercentageNumber),
TransformOrigin(BackgroundPosition),
Margin(Length<false>),
MarginX(Length<false>),
MarginY(Length<false>),
MarginTop(Length<false>),
MarginRight(Length<false>),
MarginBottom(Length<false>),
MarginLeft(Length<false>),
Padding(Length<false>),
PaddingX(Length<false>),
PaddingY(Length<false>),
PaddingTop(Length<false>),
PaddingRight(Length<false>),
PaddingBottom(Length<false>),
PaddingLeft(Length<false>),
Inset(Length),
InsetX(Length),
InsetY(Length),
Top(Length),
Right(Length),
Bottom(Length),
Left(Length),
Blur(TwBlur),
Brightness(PercentageNumber),
Contrast(PercentageNumber),
DropShadow(TextShadow),
Grayscale(PercentageNumber),
HueRotate(Angle),
Invert(PercentageNumber),
Saturate(PercentageNumber),
Sepia(PercentageNumber),
Filter(Filters),
BackdropBlur(TwBlur),
BackdropBrightness(PercentageNumber),
BackdropContrast(PercentageNumber),
BackdropGrayscale(PercentageNumber),
BackdropHueRotate(Angle),
BackdropInvert(PercentageNumber),
BackdropOpacity(PercentageNumber),
BackdropSaturate(PercentageNumber),
BackdropSepia(PercentageNumber),
BackdropFilter(Filters),
TextShadow(TextShadow),
Isolation(Isolation),
MixBlendMode(BlendMode),
BackgroundBlendMode(BlendMode),
Visibility(Visibility),
VerticalAlign(VerticalAlign),
BgLinearAngle(Angle),
BgRadial,
BgConicAngle(Angle),
GradientFrom(ColorInput),
GradientTo(ColorInput),
GradientVia(ColorInput),
}
fn extract_arbitrary_value(suffix: &str) -> Option<Cow<'_, str>> {
if suffix.starts_with('[') && suffix.ends_with(']') {
let value = &suffix[1..suffix.len() - 1];
if value.contains('_') {
Some(Cow::Owned(value.replace('_', " ")))
} else {
Some(Cow::Borrowed(value))
}
} else {
None
}
}
pub trait TailwindPropertyParser: Sized + for<'i> FromCss<'i> {
fn parse_tw(token: &str) -> Option<Self>;
fn parse_tw_with_arbitrary(token: &str) -> Option<Self> {
if let Some(value) = extract_arbitrary_value(token) {
return Self::from_str(&value).ok();
}
Self::parse_tw(token)
}
}
impl Neg for TailwindProperty {
type Output = Self;
fn neg(self) -> Self::Output {
match self {
TailwindProperty::Margin(length) => TailwindProperty::Margin(-length),
TailwindProperty::MarginX(length) => TailwindProperty::MarginX(-length),
TailwindProperty::MarginY(length) => TailwindProperty::MarginY(-length),
TailwindProperty::MarginTop(length) => TailwindProperty::MarginTop(-length),
TailwindProperty::MarginRight(length) => TailwindProperty::MarginRight(-length),
TailwindProperty::MarginBottom(length) => TailwindProperty::MarginBottom(-length),
TailwindProperty::MarginLeft(length) => TailwindProperty::MarginLeft(-length),
TailwindProperty::Padding(length) => TailwindProperty::Padding(-length),
TailwindProperty::PaddingX(length) => TailwindProperty::PaddingX(-length),
TailwindProperty::PaddingY(length) => TailwindProperty::PaddingY(-length),
TailwindProperty::PaddingTop(length) => TailwindProperty::PaddingTop(-length),
TailwindProperty::PaddingRight(length) => TailwindProperty::PaddingRight(-length),
TailwindProperty::PaddingBottom(length) => TailwindProperty::PaddingBottom(-length),
TailwindProperty::PaddingLeft(length) => TailwindProperty::PaddingLeft(-length),
TailwindProperty::Inset(length) => TailwindProperty::Inset(-length),
TailwindProperty::InsetX(length) => TailwindProperty::InsetX(-length),
TailwindProperty::InsetY(length) => TailwindProperty::InsetY(-length),
TailwindProperty::Top(length) => TailwindProperty::Top(-length),
TailwindProperty::Right(length) => TailwindProperty::Right(-length),
TailwindProperty::Bottom(length) => TailwindProperty::Bottom(-length),
TailwindProperty::Left(length) => TailwindProperty::Left(-length),
TailwindProperty::Translate(length) => TailwindProperty::Translate(-length),
TailwindProperty::TranslateX(length) => TailwindProperty::TranslateX(-length),
TailwindProperty::TranslateY(length) => TailwindProperty::TranslateY(-length),
TailwindProperty::Scale(percentage_number) => TailwindProperty::Scale(-percentage_number),
TailwindProperty::ScaleX(percentage_number) => TailwindProperty::ScaleX(-percentage_number),
TailwindProperty::ScaleY(percentage_number) => TailwindProperty::ScaleY(-percentage_number),
TailwindProperty::Rotate(angle) => TailwindProperty::Rotate(-angle),
TailwindProperty::LetterSpacing(length) => TailwindProperty::LetterSpacing(-length),
TailwindProperty::HueRotate(angle) => TailwindProperty::HueRotate(-angle),
TailwindProperty::BackdropHueRotate(angle) => TailwindProperty::BackdropHueRotate(-angle),
_ => self,
}
}
}
macro_rules! append_filter {
($style:expr, $field:ident, $filter:expr) => {{
if let crate::layout::style::CssValue::Value(existing_filters) = &mut $style.$field {
existing_filters.push($filter);
} else {
$style.$field = vec![$filter].into();
}
}};
}
impl TailwindProperty {
pub fn parse(token: &str) -> Option<TailwindProperty> {
if let Some(property) = FIXED_PROPERTIES.get(token) {
return Some(property.clone());
}
if let Some(stripped) = token.strip_prefix('-') {
if let Some(property) = Self::parse_prefix_suffix(stripped) {
return Some(-property);
}
return None;
}
Self::parse_prefix_suffix(token)
}
fn parse_prefix_suffix(token: &str) -> Option<TailwindProperty> {
let dash_positions = token.match_indices('-').map(|(i, _)| i);
for dash_pos in dash_positions.rev() {
let prefix = &token[..dash_pos];
let Some(parsers) = PREFIX_PARSERS.get(prefix) else {
continue;
};
let suffix = &token[dash_pos + 1..];
for parser in *parsers {
if let Some(property) = parser.parse(suffix) {
return Some(property);
}
}
}
None
}
pub(crate) fn apply(&self, style: &mut Style, gradient_state: &mut TwGradientState) {
match *self {
TailwindProperty::BgLinearAngle(angle) => {
gradient_state.gradient_type = TwGradientType::Linear;
gradient_state.angle = Some(angle);
}
TailwindProperty::BgRadial => {
gradient_state.gradient_type = TwGradientType::Radial;
}
TailwindProperty::BgConicAngle(angle) => {
gradient_state.gradient_type = TwGradientType::Conic;
gradient_state.angle = Some(angle);
}
TailwindProperty::GradientFrom(color) => {
gradient_state.from = Some(color);
}
TailwindProperty::GradientTo(color) => {
gradient_state.to = Some(color);
}
TailwindProperty::GradientVia(color) => {
gradient_state.via = Some(color);
}
TailwindProperty::BackgroundClip(background_clip) => {
style.background_clip = background_clip.into();
}
TailwindProperty::Gap(gap) => {
style.gap = SpacePair::from_single(gap).into();
}
TailwindProperty::GapX(gap_x) => {
style.column_gap = Some(gap_x).into();
}
TailwindProperty::GapY(gap_y) => {
style.row_gap = Some(gap_y).into();
}
TailwindProperty::BoxSizing(box_sizing) => {
style.box_sizing = box_sizing.into();
}
TailwindProperty::FlexGrow(flex_grow) => {
style.flex_grow = Some(flex_grow).into();
}
TailwindProperty::FlexShrink(flex_shrink) => {
style.flex_shrink = Some(flex_shrink).into();
}
TailwindProperty::Aspect(ratio) => {
style.aspect_ratio = ratio.into();
}
TailwindProperty::Items(align_items) => {
style.align_items = align_items.into();
}
TailwindProperty::Justify(justify_content) => {
style.justify_content = justify_content.into();
}
TailwindProperty::Content(align_content) => {
style.align_content = align_content.into();
}
TailwindProperty::AlignSelf(align_self) => {
style.align_self = align_self.into();
}
TailwindProperty::FlexDirection(flex_direction) => {
style.flex_direction = flex_direction.into();
}
TailwindProperty::FlexWrap(flex_wrap) => {
style.flex_wrap = flex_wrap.into();
}
TailwindProperty::Flex(flex) => {
style.flex = Some(flex).into();
}
TailwindProperty::FlexBasis(flex_basis) => {
style.flex_basis = Some(flex_basis).into();
}
TailwindProperty::Overflow(overflow) => {
style.overflow = SpacePair::from_single(overflow).into();
}
TailwindProperty::Position(position) => {
style.position = position.into();
}
TailwindProperty::FontStyle(font_style) => {
style.font_style = font_style.into();
}
TailwindProperty::FontWeight(font_weight) => {
style.font_weight = font_weight.into();
}
TailwindProperty::FontStretch(font_stretch) => {
style.font_stretch = font_stretch.into();
}
TailwindProperty::FontFamily(ref font_family) => {
style.font_family = Some(font_family.clone()).into();
}
TailwindProperty::LineClamp(ref line_clamp) => {
style.line_clamp = Some(line_clamp.clone()).into();
}
TailwindProperty::TextAlign(text_align) => {
style.text_align = text_align.into();
}
TailwindProperty::TextDecorationLine(text_decoration) => {
style.text_decoration_line = text_decoration.into();
}
TailwindProperty::TextDecorationColor(color_input) => {
style.text_decoration_color = Some(color_input).into();
}
TailwindProperty::TextDecorationThickness(thickness) => {
style.text_decoration_thickness = Some(thickness).into();
}
TailwindProperty::TextTransform(text_transform) => {
style.text_transform = text_transform.into();
}
TailwindProperty::Size(size) => {
style.width = size.into();
style.height = size.into();
}
TailwindProperty::Width(width) => {
style.width = width.into();
}
TailwindProperty::Height(height) => {
style.height = height.into();
}
TailwindProperty::MinWidth(min_width) => {
style.min_width = min_width.into();
}
TailwindProperty::MinHeight(min_height) => {
style.min_height = min_height.into();
}
TailwindProperty::MaxWidth(max_width) => {
style.max_width = max_width.into();
}
TailwindProperty::MaxHeight(max_height) => {
style.max_height = max_height.into();
}
TailwindProperty::Shadow(box_shadow) => {
style.box_shadow = [box_shadow].into();
}
TailwindProperty::Display(display) => {
style.display = display.into();
}
TailwindProperty::OverflowX(overflow) => {
style.overflow_x = Some(overflow).into();
}
TailwindProperty::OverflowY(overflow) => {
style.overflow_y = Some(overflow).into();
}
TailwindProperty::ObjectPosition(background_position) => {
style.object_position = background_position.into();
}
TailwindProperty::ObjectFit(object_fit) => {
style.object_fit = object_fit.into();
}
TailwindProperty::BackgroundPosition(background_position) => {
style.background_position = [background_position].into();
}
TailwindProperty::BackgroundSize(background_size) => {
style.background_size = [background_size].into();
}
TailwindProperty::BackgroundRepeat(background_repeat) => {
style.background_repeat = [background_repeat].into();
}
TailwindProperty::BackgroundImage(ref background_image) => {
style.background_image = [background_image.clone()].into();
}
TailwindProperty::BorderDefault => {
style.border_width = Some(Sides([Length::Px(1.0); 4])).into();
style.border_style = Some(BorderStyle::Solid).into();
}
TailwindProperty::BorderWidth(tw_border_width) => {
style.border_width = Some(Sides([tw_border_width.0; 4])).into();
}
TailwindProperty::BorderStyle(border_style) => {
style.border_style = Some(border_style).into();
}
TailwindProperty::JustifySelf(align_items) => {
style.justify_self = align_items.into();
}
TailwindProperty::JustifyItems(align_items) => {
style.justify_items = align_items.into();
}
TailwindProperty::Color(color_input) => {
style.color = color_input.into();
}
TailwindProperty::Opacity(percentage_number) => {
style.opacity = percentage_number.into();
}
TailwindProperty::BackgroundColor(color_input) => {
style.background_color = color_input.into();
}
TailwindProperty::BorderColor(color_input) => {
style.border_color = Some(color_input).into();
}
TailwindProperty::BorderTopWidth(tw_border_width) => {
style.border_top_width = Some(tw_border_width.0).into();
}
TailwindProperty::BorderRightWidth(tw_border_width) => {
style.border_right_width = Some(tw_border_width.0).into();
}
TailwindProperty::BorderBottomWidth(tw_border_width) => {
style.border_bottom_width = Some(tw_border_width.0).into();
}
TailwindProperty::BorderLeftWidth(tw_border_width) => {
style.border_left_width = Some(tw_border_width.0).into();
}
TailwindProperty::BorderXWidth(tw_border_width) => {
style.border_left_width = Some(tw_border_width.0).into();
style.border_right_width = Some(tw_border_width.0).into();
}
TailwindProperty::BorderYWidth(tw_border_width) => {
style.border_top_width = Some(tw_border_width.0).into();
style.border_bottom_width = Some(tw_border_width.0).into();
}
TailwindProperty::OutlineDefault => {
style.outline_width = Some(Length::Px(1.0)).into();
style.outline_style = Some(BorderStyle::Solid).into();
}
TailwindProperty::OutlineWidth(tw_border_width) => {
style.outline_width = Some(tw_border_width.0).into();
}
TailwindProperty::OutlineColor(color_input) => {
style.outline_color = Some(color_input).into();
}
TailwindProperty::OutlineStyle(outline_style) => {
style.outline_style = Some(outline_style).into();
}
TailwindProperty::OutlineOffset(outline_offset) => {
style.outline_offset = Some(outline_offset.0).into();
}
TailwindProperty::Rounded(rounded) => {
style.border_radius = BorderRadius(Sides([SpacePair::from_single(rounded.0); 4])).into();
}
TailwindProperty::VerticalAlign(vertical_align) => {
style.vertical_align = vertical_align.into();
}
TailwindProperty::RoundedTopLeft(rounded) => {
style.border_top_left_radius = Some(SpacePair::from_single(rounded.0)).into();
}
TailwindProperty::RoundedTopRight(rounded) => {
style.border_top_right_radius = Some(SpacePair::from_single(rounded.0)).into();
}
TailwindProperty::RoundedBottomRight(rounded) => {
style.border_bottom_right_radius = Some(SpacePair::from_single(rounded.0)).into();
}
TailwindProperty::RoundedBottomLeft(rounded) => {
style.border_bottom_left_radius = Some(SpacePair::from_single(rounded.0)).into();
}
TailwindProperty::RoundedTop(rounded) => {
style.border_top_left_radius = Some(SpacePair::from_single(rounded.0)).into();
style.border_top_right_radius = Some(SpacePair::from_single(rounded.0)).into();
}
TailwindProperty::RoundedRight(rounded) => {
style.border_top_right_radius = Some(SpacePair::from_single(rounded.0)).into();
style.border_bottom_right_radius = Some(SpacePair::from_single(rounded.0)).into();
}
TailwindProperty::RoundedBottom(rounded) => {
style.border_bottom_left_radius = Some(SpacePair::from_single(rounded.0)).into();
style.border_bottom_right_radius = Some(SpacePair::from_single(rounded.0)).into();
}
TailwindProperty::RoundedLeft(rounded) => {
style.border_top_left_radius = Some(SpacePair::from_single(rounded.0)).into();
style.border_bottom_left_radius = Some(SpacePair::from_single(rounded.0)).into();
}
TailwindProperty::TextOverflow(ref text_overflow) => {
style.text_overflow = text_overflow.clone().into();
}
TailwindProperty::Truncate => {
style.text_overflow = TextOverflow::Ellipsis.into();
style.white_space = WhiteSpace {
text_wrap_mode: TextWrapMode::NoWrap,
white_space_collapse: WhiteSpaceCollapse::Collapse,
}
.into();
style.overflow = SpacePair::from_single(Overflow::Hidden).into();
}
TailwindProperty::TextWrap(text_wrap) => {
style.text_wrap = text_wrap.into();
}
TailwindProperty::WhiteSpace(white_space) => {
style.white_space = white_space.into();
}
TailwindProperty::WordBreak(word_break) => {
style.word_break = word_break.into();
}
TailwindProperty::Isolation(isolation) => {
style.isolation = isolation.into();
}
TailwindProperty::MixBlendMode(blend_mode) => {
style.mix_blend_mode = blend_mode.into();
}
TailwindProperty::BackgroundBlendMode(blend_mode) => {
style.background_blend_mode = [blend_mode].into();
}
TailwindProperty::OverflowWrap(overflow_wrap) => {
style.overflow_wrap = overflow_wrap.into();
}
TailwindProperty::FontSize(font_size) => {
style.font_size = Some(font_size.font_size).into();
if let Some(line_height) = font_size.line_height {
style.line_height = line_height.into();
}
}
TailwindProperty::LineHeight(line_height) => {
style.line_height = line_height.into();
}
TailwindProperty::Translate(length) => {
style.translate = Some(SpacePair::from_single(length)).into();
}
TailwindProperty::TranslateX(length) => {
style.translate_x = Some(length).into();
}
TailwindProperty::TranslateY(length) => {
style.translate_y = Some(length).into();
}
TailwindProperty::Rotate(angle) => {
style.rotate = Some(angle).into();
}
TailwindProperty::Scale(percentage_number) => {
style.scale = Some(SpacePair::from_single(percentage_number)).into();
}
TailwindProperty::ScaleX(percentage_number) => {
style.scale_x = Some(percentage_number).into();
}
TailwindProperty::ScaleY(percentage_number) => {
style.scale_y = Some(percentage_number).into();
}
TailwindProperty::TransformOrigin(background_position) => {
style.transform_origin = Some(background_position).into();
}
TailwindProperty::Margin(length) => {
style.margin = Sides([length; 4]).into();
}
TailwindProperty::MarginX(length) => {
style.margin_inline = Some(SpacePair::from_single(length)).into();
}
TailwindProperty::MarginY(length) => {
style.margin_block = Some(SpacePair::from_single(length)).into();
}
TailwindProperty::MarginTop(length) => {
style.margin_top = Some(length).into();
}
TailwindProperty::MarginRight(length) => {
style.margin_right = Some(length).into();
}
TailwindProperty::MarginBottom(length) => {
style.margin_bottom = Some(length).into();
}
TailwindProperty::MarginLeft(length) => {
style.margin_left = Some(length).into();
}
TailwindProperty::Padding(length) => {
style.padding = Sides([length; 4]).into();
}
TailwindProperty::PaddingX(length) => {
style.padding_inline = Some(SpacePair::from_single(length)).into();
}
TailwindProperty::PaddingY(length) => {
style.padding_block = Some(SpacePair::from_single(length)).into();
}
TailwindProperty::PaddingTop(length) => {
style.padding_top = Some(length).into();
}
TailwindProperty::PaddingRight(length) => {
style.padding_right = Some(length).into();
}
TailwindProperty::PaddingBottom(length) => {
style.padding_bottom = Some(length).into();
}
TailwindProperty::PaddingLeft(length) => {
style.padding_left = Some(length).into();
}
TailwindProperty::Inset(length) => {
style.inset = Sides([length; 4]).into();
}
TailwindProperty::InsetX(length) => {
style.inset_inline = Some(SpacePair::from_single(length)).into();
}
TailwindProperty::InsetY(length) => {
style.inset_block = Some(SpacePair::from_single(length)).into();
}
TailwindProperty::Top(length) => {
style.top = Some(length).into();
}
TailwindProperty::Right(length) => {
style.right = Some(length).into();
}
TailwindProperty::Bottom(length) => {
style.bottom = Some(length).into();
}
TailwindProperty::Left(length) => {
style.left = Some(length).into();
}
TailwindProperty::GridAutoColumns(grid_auto_size) => {
style.grid_auto_columns = Some([grid_auto_size].into()).into();
}
TailwindProperty::GridAutoRows(grid_auto_size) => {
style.grid_auto_rows = Some([grid_auto_size].into()).into();
}
TailwindProperty::GridColumn(ref tw_grid_span) => {
style.grid_column = Some(tw_grid_span.clone()).into();
}
TailwindProperty::GridRow(ref tw_grid_span) => {
style.grid_row = Some(tw_grid_span.clone()).into();
}
TailwindProperty::GridColumnStart(ref tw_grid_placement) => {
if let CssValue::Value(Some(ref mut existing_grid_column)) = style.grid_column {
existing_grid_column.start = tw_grid_placement.clone();
} else {
style.grid_column = Some(GridLine::start(tw_grid_placement.clone())).into();
}
}
TailwindProperty::GridColumnEnd(ref tw_grid_placement) => {
if let CssValue::Value(Some(ref mut existing_grid_column)) = style.grid_column {
existing_grid_column.end = tw_grid_placement.clone();
} else {
style.grid_column = Some(GridLine::end(tw_grid_placement.clone())).into();
}
}
TailwindProperty::GridRowStart(ref tw_grid_placement) => {
if let CssValue::Value(Some(ref mut existing_grid_row)) = style.grid_row {
existing_grid_row.start = tw_grid_placement.clone();
} else {
style.grid_row = Some(GridLine::start(tw_grid_placement.clone())).into();
}
}
TailwindProperty::GridRowEnd(ref tw_grid_placement) => {
if let CssValue::Value(Some(ref mut existing_grid_row)) = style.grid_row {
existing_grid_row.end = tw_grid_placement.clone();
} else {
style.grid_row = Some(GridLine::end(tw_grid_placement.clone())).into();
}
}
TailwindProperty::GridTemplateColumns(ref tw_grid_template) => {
style.grid_template_columns = Some(tw_grid_template.0.clone()).into();
}
TailwindProperty::GridTemplateRows(ref tw_grid_template) => {
style.grid_template_rows = Some(tw_grid_template.0.clone()).into();
}
TailwindProperty::LetterSpacing(tw_letter_spacing) => {
style.letter_spacing = Some(tw_letter_spacing.0).into();
}
TailwindProperty::GridAutoFlow(grid_auto_flow) => {
style.grid_auto_flow = Some(grid_auto_flow).into();
}
TailwindProperty::GridColumnSpan(grid_placement_span) => {
style.grid_column = Some(GridLine::span(grid_placement_span)).into();
}
TailwindProperty::GridRowSpan(grid_placement_span) => {
style.grid_row = Some(GridLine::span(grid_placement_span)).into();
}
TailwindProperty::Blur(tw_blur) => {
append_filter!(style, filter, Filter::Blur(tw_blur.0));
}
TailwindProperty::Brightness(percentage_number) => {
append_filter!(style, filter, Filter::Brightness(percentage_number));
}
TailwindProperty::Contrast(percentage_number) => {
append_filter!(style, filter, Filter::Contrast(percentage_number));
}
TailwindProperty::DropShadow(text_shadow) => {
append_filter!(style, filter, Filter::DropShadow(text_shadow));
}
TailwindProperty::Grayscale(percentage_number) => {
append_filter!(style, filter, Filter::Grayscale(percentage_number));
}
TailwindProperty::HueRotate(angle) => {
append_filter!(style, filter, Filter::HueRotate(angle));
}
TailwindProperty::Invert(percentage_number) => {
append_filter!(style, filter, Filter::Invert(percentage_number));
}
TailwindProperty::Saturate(percentage_number) => {
append_filter!(style, filter, Filter::Saturate(percentage_number));
}
TailwindProperty::Sepia(percentage_number) => {
append_filter!(style, filter, Filter::Sepia(percentage_number));
}
TailwindProperty::Filter(ref filters) => {
for f in filters {
append_filter!(style, filter, *f);
}
}
TailwindProperty::BackdropBlur(tw_blur) => {
append_filter!(style, backdrop_filter, Filter::Blur(tw_blur.0));
}
TailwindProperty::BackdropBrightness(percentage_number) => {
append_filter!(
style,
backdrop_filter,
Filter::Brightness(percentage_number)
);
}
TailwindProperty::BackdropContrast(percentage_number) => {
append_filter!(style, backdrop_filter, Filter::Contrast(percentage_number));
}
TailwindProperty::BackdropGrayscale(percentage_number) => {
append_filter!(style, backdrop_filter, Filter::Grayscale(percentage_number));
}
TailwindProperty::BackdropHueRotate(angle) => {
append_filter!(style, backdrop_filter, Filter::HueRotate(angle));
}
TailwindProperty::BackdropInvert(percentage_number) => {
append_filter!(style, backdrop_filter, Filter::Invert(percentage_number));
}
TailwindProperty::BackdropOpacity(percentage_number) => {
append_filter!(style, backdrop_filter, Filter::Opacity(percentage_number));
}
TailwindProperty::BackdropSaturate(percentage_number) => {
append_filter!(style, backdrop_filter, Filter::Saturate(percentage_number));
}
TailwindProperty::BackdropSepia(percentage_number) => {
append_filter!(style, backdrop_filter, Filter::Sepia(percentage_number));
}
TailwindProperty::BackdropFilter(ref filters) => {
for f in filters {
append_filter!(style, backdrop_filter, *f);
}
}
TailwindProperty::TextShadow(text_shadow) => {
style.text_shadow = Some([text_shadow].into()).into();
}
TailwindProperty::Visibility(visibility) => {
style.visibility = visibility.into();
}
}
}
}
#[cfg(test)]
mod tests {
use crate::layout::style::{CssValue, Style, properties::BackgroundImage};
use super::*;
#[test]
fn test_box_sizing() {
assert_eq!(
TailwindProperty::parse("box-border"),
Some(TailwindProperty::BoxSizing(BoxSizing::BorderBox))
);
}
#[test]
fn test_parse_width() {
assert_eq!(
TailwindProperty::parse("w-64"),
Some(TailwindProperty::Width(Length::Rem(64.0 * TW_VAR_SPACING)))
);
assert_eq!(
TailwindProperty::parse("h-32"),
Some(TailwindProperty::Height(Length::Rem(32.0 * TW_VAR_SPACING)))
);
assert_eq!(
TailwindProperty::parse("justify-self-center"),
Some(TailwindProperty::JustifySelf(AlignItems::Center))
);
}
#[test]
fn test_parse_color() {
assert_eq!(
TailwindProperty::parse("text-black/30"),
Some(TailwindProperty::Color(ColorInput::Value(Color([
0,
0,
0,
(0.3_f32 * 255.0).round() as u8
]))))
);
}
#[test]
fn test_parse_decoration_color() {
assert_eq!(
TailwindProperty::parse("decoration-red-500"),
Some(TailwindProperty::TextDecorationColor(ColorInput::Value(
Color([239, 68, 68, 255])
)))
);
}
#[test]
fn test_parse_text_decoration_lines() {
assert_eq!(
TailwindProperty::parse("underline"),
Some(TailwindProperty::TextDecorationLine(
TextDecorationLines::UNDERLINE
))
);
assert_eq!(
TailwindProperty::parse("no-underline"),
Some(TailwindProperty::TextDecorationLine(
TextDecorationLines::empty()
))
);
}
#[test]
fn test_parse_arbitrary_color() {
assert_eq!(
TailwindProperty::parse("text-[rgb(0, 191, 255)]"),
Some(TailwindProperty::Color(ColorInput::Value(Color([
0, 191, 255, 255
]))))
);
}
#[test]
fn test_parse_arbitrary_flex_with_spaces() {
assert_eq!(
TailwindProperty::parse("flex-[3_1_auto]"),
Some(TailwindProperty::Flex(Flex {
grow: 3.0,
shrink: 1.0,
basis: Length::Auto,
}))
);
}
#[test]
fn test_parse_negative_margin() {
assert_eq!(
TailwindProperty::parse("-ml-4"),
Some(TailwindProperty::MarginLeft(Length::Rem(
-4.0 * TW_VAR_SPACING
)))
);
}
#[test]
fn test_parse_border_radius() {
assert_eq!(
TailwindProperty::parse("rounded-xs"),
Some(TailwindProperty::Rounded(TwRounded(Length::Rem(0.125))))
);
assert_eq!(
TailwindProperty::parse("rounded-full"),
Some(TailwindProperty::Rounded(TwRounded(Length::Px(9999.0))))
);
}
#[test]
fn test_parse_font_size_with_arbitrary_line_height() {
assert_eq!(
TailwindProperty::parse("text-base/[12.34]"),
Some(TailwindProperty::FontSize(TwFontSize {
font_size: Length::Rem(1.0),
line_height: Some(LineHeight::Unitless(12.34)),
}))
);
}
#[test]
fn test_parse_border_width() {
assert_eq!(
TailwindProperty::parse("border"),
Some(TailwindProperty::BorderDefault)
);
assert_eq!(
TailwindProperty::parse("border-t-2"),
Some(TailwindProperty::BorderTopWidth(TwBorderWidth(Length::Px(
2.0
))))
);
assert_eq!(
TailwindProperty::parse("border-x-4"),
Some(TailwindProperty::BorderXWidth(TwBorderWidth(Length::Px(
4.0
))))
);
assert_eq!(
TailwindProperty::parse("border-solid"),
Some(TailwindProperty::BorderStyle(BorderStyle::Solid))
);
assert_eq!(
TailwindProperty::parse("border-none"),
Some(TailwindProperty::BorderStyle(BorderStyle::None))
);
}
#[test]
fn test_parse_outline() {
assert_eq!(
TailwindProperty::parse("outline"),
Some(TailwindProperty::OutlineDefault)
);
assert_eq!(
TailwindProperty::parse("outline-2"),
Some(TailwindProperty::OutlineWidth(TwBorderWidth(Length::Px(
2.0
))))
);
assert_eq!(
TailwindProperty::parse("outline-red-500"),
Some(TailwindProperty::OutlineColor(ColorInput::Value(Color([
239, 68, 68, 255
]))))
);
assert_eq!(
TailwindProperty::parse("outline-solid"),
Some(TailwindProperty::OutlineStyle(BorderStyle::Solid))
);
assert_eq!(
TailwindProperty::parse("outline-offset-4"),
Some(TailwindProperty::OutlineOffset(TwBorderWidth(Length::Px(
4.0
))))
);
assert_eq!(
TailwindProperty::parse("outline-none"),
Some(TailwindProperty::OutlineStyle(BorderStyle::None))
);
}
#[test]
fn test_parse_col_end() {
assert_eq!(
TailwindProperty::parse("col-end-1"),
Some(TailwindProperty::GridColumnEnd(GridPlacement::Line(1)))
);
}
#[test]
fn test_parse_overflow_clip() {
assert_eq!(
TailwindProperty::parse("overflow-clip"),
Some(TailwindProperty::Overflow(Overflow::Clip))
);
assert_eq!(
TailwindProperty::parse("overflow-x-clip"),
Some(TailwindProperty::OverflowX(Overflow::Clip))
);
assert_eq!(
TailwindProperty::parse("overflow-y-clip"),
Some(TailwindProperty::OverflowY(Overflow::Clip))
);
}
#[test]
fn test_comprehensive_mappings() {
let should_parse = vec![
"flex",
"grid",
"hidden",
"block",
"inline",
"w-4",
"h-8",
"size-12",
"min-w-0",
"max-h-96",
"m-2",
"mx-4",
"my-auto",
"mt-8",
"mr-6",
"mb-4",
"ml-2",
"p-3",
"px-5",
"py-2",
"pt-1",
"pr-4",
"pb-3",
"pl-2",
"text-red-500",
"bg-blue-200",
"border-gray-300",
"text-sm",
"font-bold",
"font-stretch-condensed",
"font-stretch-ultra-expanded",
"font-stretch-75%",
"uppercase",
"tracking-wide",
"justify-center",
"items-end",
"self-start",
"flex-grow",
"shrink",
"border",
"border-t-2",
"border-solid",
"border-none",
"outline",
"outline-2",
"outline-red-500",
"outline-solid",
"outline-offset-2",
"rounded-lg",
"rotate-45",
"scale-75",
"translate-x-4",
"grid-cols-3",
"col-span-2",
"backdrop-blur-md",
"backdrop-brightness-50",
"backdrop-contrast-125",
"backdrop-grayscale",
"backdrop-hue-rotate-90",
"backdrop-invert",
"backdrop-opacity-50",
"backdrop-saturate-200",
"backdrop-sepia",
"backdrop-filter-[blur(4px)_brightness(0.5)]",
];
let should_not_parse = vec!["nonexistent-class", "invalid-prefix-1", "random-string"];
for class in should_parse {
assert!(
TailwindProperty::parse(class).is_some(),
"Expected '{}' to parse successfully",
class
);
}
for class in should_not_parse {
assert!(
TailwindProperty::parse(class).is_none(),
"Expected '{}' to fail parsing",
class
);
}
}
#[test]
fn test_breakpoint_matches() {
let viewport = (1000, 1000).into();
assert!(Breakpoint::parse("sm").is_some_and(|bp| bp.matches(viewport)));
}
#[test]
fn test_breakpoint_does_not_match() {
let viewport = (1000, 1000).into();
assert!(Breakpoint::parse("xl").is_some_and(|bp| !bp.matches(viewport)));
}
#[test]
fn test_value_parsing() {
assert_eq!(
TailwindValue::parse("md:!mt-4"),
Some(TailwindValue {
property: TailwindProperty::MarginTop(Length::Rem(1.0)),
breakpoint: Some(Breakpoint(Length::Rem(48.0))),
important: true,
})
);
}
#[test]
fn test_values_sorting() {
assert_eq!(
TailwindValues::from_str("md:!mt-4 sm:mt-8 !mt-12 mt-16"),
Ok(TailwindValues {
inner: vec![
TailwindValue {
property: TailwindProperty::MarginTop(Length::Rem(4.0)),
breakpoint: None,
important: false,
},
TailwindValue {
property: TailwindProperty::MarginTop(Length::Rem(2.0)),
breakpoint: Some(Breakpoint(Length::Rem(40.0))),
important: false,
},
TailwindValue {
property: TailwindProperty::MarginTop(Length::Rem(3.0)),
breakpoint: None,
important: true,
},
TailwindValue {
property: TailwindProperty::MarginTop(Length::Rem(1.0)),
breakpoint: Some(Breakpoint(Length::Rem(48.0))),
important: true,
},
]
})
)
}
#[test]
fn test_filters_append() {
use crate::layout::style::{CssValue, Style, properties::Filter};
let mut style = Style::default();
let mut gradient_state = TwGradientState::default();
if let Some(blur_prop) = TailwindProperty::parse("blur-sm") {
blur_prop.apply(&mut style, &mut gradient_state);
}
if let Some(brightness_prop) = TailwindProperty::parse("brightness-150") {
brightness_prop.apply(&mut style, &mut gradient_state);
}
if let Some(contrast_prop) = TailwindProperty::parse("contrast-125") {
contrast_prop.apply(&mut style, &mut gradient_state);
}
assert_eq!(
style.filter,
CssValue::Value(vec![
Filter::Blur(Length::Px(8.0)),
Filter::Brightness(PercentageNumber(1.5)),
Filter::Contrast(PercentageNumber(1.25))
])
)
}
#[test]
fn test_parse_blend_mode() {
assert_eq!(
TailwindProperty::parse("mix-blend-multiply"),
Some(TailwindProperty::MixBlendMode(BlendMode::Multiply))
);
assert_eq!(
TailwindProperty::parse("bg-blend-screen"),
Some(TailwindProperty::BackgroundBlendMode(BlendMode::Screen))
);
}
#[test]
fn test_parse_vertical_align() {
assert_eq!(
TailwindProperty::parse("align-baseline"),
Some(TailwindProperty::VerticalAlign(VerticalAlign::Baseline))
);
assert_eq!(
TailwindProperty::parse("align-top"),
Some(TailwindProperty::VerticalAlign(VerticalAlign::Top))
);
assert_eq!(
TailwindProperty::parse("align-middle"),
Some(TailwindProperty::VerticalAlign(VerticalAlign::Middle))
);
assert_eq!(
TailwindProperty::parse("align-bottom"),
Some(TailwindProperty::VerticalAlign(VerticalAlign::Bottom))
);
assert_eq!(
TailwindProperty::parse("align-text-top"),
Some(TailwindProperty::VerticalAlign(VerticalAlign::TextTop))
);
assert_eq!(
TailwindProperty::parse("align-text-bottom"),
Some(TailwindProperty::VerticalAlign(VerticalAlign::TextBottom))
);
assert_eq!(
TailwindProperty::parse("align-sub"),
Some(TailwindProperty::VerticalAlign(VerticalAlign::Sub))
);
assert_eq!(
TailwindProperty::parse("align-super"),
Some(TailwindProperty::VerticalAlign(VerticalAlign::Super))
);
}
#[test]
fn test_parse_decoration_thickness() {
assert_eq!(
TailwindProperty::parse("decoration-4"),
Some(TailwindProperty::TextDecorationThickness(
TextDecorationThickness::Length(Length::Px(4.0))
))
);
assert_eq!(
TailwindProperty::parse("decoration-auto"),
Some(TailwindProperty::TextDecorationThickness(
TextDecorationThickness::Length(Length::Auto)
))
);
assert_eq!(
TailwindProperty::parse("decoration-from-font"),
Some(TailwindProperty::TextDecorationThickness(
TextDecorationThickness::FromFont
))
);
assert_eq!(
TailwindProperty::parse("decoration-[3px]"),
Some(TailwindProperty::TextDecorationThickness(
TextDecorationThickness::Length(Length::Px(3.0))
))
);
}
#[test]
fn test_linear_gradient_apply() {
let mut style = Style::default();
let viewport = (100, 100).into();
let Ok(values) =
TailwindValues::from_str("bg-linear-to-r from-red-500 via-green-500 to-blue-500")
else {
unreachable!()
};
values.apply(&mut style, viewport);
assert_eq!(
style.background_image,
CssValue::Value(Some(
[BackgroundImage::Linear(LinearGradient {
angle: Angle::new(90.0),
stops: [
GradientStop::ColorHint {
color: ColorInput::Value(Color([239, 68, 68, 255])),
hint: Some(StopPosition(Length::Percentage(0.0))),
},
GradientStop::ColorHint {
color: ColorInput::Value(Color([34, 197, 94, 255])),
hint: Some(StopPosition(Length::Percentage(50.0))),
},
GradientStop::ColorHint {
color: ColorInput::Value(Color([59, 130, 246, 255])),
hint: Some(StopPosition(Length::Percentage(100.0))),
},
]
.into(),
})]
.into()
))
);
}
}