mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-12-18 08:33:07 +00:00
2411 lines
74 KiB
Rust
2411 lines
74 KiB
Rust
//! Implementation of `syn::parse::Parse` for styles, and associated helper data/functions.
|
|
// TODO make all parsers use HyphenWord where appropriate.
|
|
// TODO make all error messages nice
|
|
// TODO 100% test coverage
|
|
// TODO see if I can get https://github.com/rust-lang/rust/issues/67544 accepted. then change "em" to
|
|
// em and "ex" to ex.
|
|
// TODO Split out extra "Dynamic" layer for each type for use in proc macro (so we can have `{ <arbitary
|
|
// rust code> }`)
|
|
use crate::*;
|
|
use proc_macro2::Span;
|
|
use std::{
|
|
cell::RefCell,
|
|
collections::BTreeSet,
|
|
fmt::{self, Write},
|
|
ops::RangeBounds,
|
|
str,
|
|
};
|
|
use syn::{
|
|
ext::IdentExt,
|
|
parse::{discouraged::Speculative, Parse, ParseStream},
|
|
punctuated::Punctuated,
|
|
spanned::Spanned,
|
|
Ident, Token,
|
|
};
|
|
|
|
use super::{DynamicStyle, DynamicStyles, Styles};
|
|
|
|
impl Parse for DynamicStyles {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let punc = s.parse_terminated::<_, Token![;]>(<DynamicStyle as Parse>::parse)?;
|
|
Ok(DynamicStyles::from(punc.into_iter().collect::<Vec<_>>()))
|
|
}
|
|
}
|
|
|
|
impl Parse for Styles {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let punc = s.parse_terminated::<_, Token![;]>(<Style as Parse>::parse)?;
|
|
Ok(Styles::from(punc.into_iter().collect::<Vec<_>>()))
|
|
}
|
|
}
|
|
|
|
impl Parse for DynamicStyle {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
// Pass through brackets
|
|
if s.peek(syn::token::Brace) {
|
|
Ok(DynamicStyle::Dynamic(s.parse()?))
|
|
} else {
|
|
Ok(DynamicStyle::Literal(s.parse()?))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for Style {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
if s.peek(syn::LitStr) {
|
|
let unchecked: syn::LitStr = s.parse()?;
|
|
return Ok(Style::Unchecked(unchecked.value()));
|
|
}
|
|
|
|
let name: HyphenWord = s.parse()?;
|
|
if name.try_match("dummy") {
|
|
return Ok(Style::Dummy);
|
|
}
|
|
|
|
s.parse::<Token![:]>()?;
|
|
|
|
let output = if name.try_match("align-content") {
|
|
Style::AlignContent(s.parse()?)
|
|
} else if name.try_match("align-items") {
|
|
Style::AlignItems(s.parse()?)
|
|
} else if name.try_match("align-self") {
|
|
Style::AlignSelf(s.parse()?)
|
|
// all
|
|
// background
|
|
} else if name.try_match("background-attachment") {
|
|
Style::BackgroundAttachment(s.parse()?)
|
|
} else if name.try_match("background-blend-mode") {
|
|
Style::BackgroundBlendMode(s.parse()?)
|
|
} else if name.try_match("background-clip") {
|
|
Style::BackgroundClip(s.parse()?)
|
|
} else if name.try_match("background-color") {
|
|
Style::BackgroundColor(s.parse()?)
|
|
} else if name.try_match("background-image") {
|
|
Style::BackgroundImage(s.parse()?)
|
|
} else if name.try_match("background-origin") {
|
|
Style::BackgroundOrigin(s.parse()?)
|
|
} else if name.try_match("background-position") {
|
|
Style::BackgroundPosition(s.parse()?)
|
|
} else if name.try_match("background-repeat") {
|
|
Style::BackgroundRepeat(s.parse()?)
|
|
} else if name.try_match("background-size") {
|
|
Style::BackgroundSize(s.parse()?)
|
|
} else if name.try_match("border") {
|
|
Style::Border(s.parse()?)
|
|
} else if name.try_match("border-bottom") {
|
|
Style::BorderBottom(s.parse()?)
|
|
} else if name.try_match("border-bottom-color") {
|
|
Style::BorderBottomColor(s.parse()?)
|
|
} else if name.try_match("border-bottom-left-radius") {
|
|
Style::BorderBottomLeftRadius(s.parse()?)
|
|
} else if name.try_match("border-bottom-right-radius") {
|
|
Style::BorderBottomRightRadius(s.parse()?)
|
|
} else if name.try_match("border-bottom-style") {
|
|
Style::BorderBottomStyle(s.parse()?)
|
|
} else if name.try_match("border-bottom-width") {
|
|
Style::BorderBottomWidth(s.parse()?)
|
|
} else if name.try_match("border-collapse") {
|
|
Style::BorderCollapse(s.parse()?)
|
|
} else if name.try_match("border-color") {
|
|
Style::BorderColor(s.parse()?)
|
|
// border-image
|
|
// border-image-outset
|
|
// border-image-repeat
|
|
// border-image-slice
|
|
// border-image-source
|
|
// border-image-width
|
|
} else if name.try_match("border-left") {
|
|
Style::BorderLeft(s.parse()?)
|
|
} else if name.try_match("border-left-color") {
|
|
Style::BorderLeftColor(s.parse()?)
|
|
} else if name.try_match("border-left-style") {
|
|
Style::BorderLeftStyle(s.parse()?)
|
|
} else if name.try_match("border-left-width") {
|
|
Style::BorderLeftWidth(s.parse()?)
|
|
} else if name.try_match("border-radius") {
|
|
Style::BorderRadius(s.parse()?)
|
|
} else if name.try_match("border-right") {
|
|
Style::BorderRight(s.parse()?)
|
|
} else if name.try_match("border-right-color") {
|
|
Style::BorderRightColor(s.parse()?)
|
|
} else if name.try_match("border-right-style") {
|
|
Style::BorderRightStyle(s.parse()?)
|
|
} else if name.try_match("border-right-width") {
|
|
Style::BorderRightWidth(s.parse()?)
|
|
// border-spacing
|
|
} else if name.try_match("border-style") {
|
|
Style::BorderStyle(s.parse()?)
|
|
} else if name.try_match("border-top") {
|
|
Style::BorderTop(s.parse()?)
|
|
} else if name.try_match("border-top-color") {
|
|
Style::BorderTopColor(s.parse()?)
|
|
} else if name.try_match("border-top-left-radius") {
|
|
Style::BorderTopLeftRadius(s.parse()?)
|
|
} else if name.try_match("border-top-right-radius") {
|
|
Style::BorderTopRightRadius(s.parse()?)
|
|
} else if name.try_match("border-top-style") {
|
|
Style::BorderTopStyle(s.parse()?)
|
|
} else if name.try_match("border-top-width") {
|
|
Style::BorderTopWidth(s.parse()?)
|
|
} else if name.try_match("border-width") {
|
|
Style::BorderWidth(s.parse()?)
|
|
} else if name.try_match("bottom") {
|
|
Style::Bottom(s.parse()?)
|
|
// box-decoration-break
|
|
} else if name.try_match("box-shadow") {
|
|
Style::BoxShadow(s.parse()?)
|
|
} else if name.try_match("box-sizing") {
|
|
Style::BoxSizing(s.parse()?)
|
|
// break-after
|
|
// break-before
|
|
// break-inside
|
|
// caption-side
|
|
// caret-color
|
|
} else if name.try_match("clear") {
|
|
Style::Clear(s.parse()?)
|
|
// clip
|
|
// clip-path
|
|
// clip-rule
|
|
} else if name.try_match("column-count") {
|
|
Style::ColumnCount(s.parse()?)
|
|
} else if name.try_match("color") {
|
|
Style::Color(s.parse()?)
|
|
// contain
|
|
// content
|
|
// counter-increment
|
|
// counter-reset
|
|
// cue
|
|
// cue-after
|
|
// cue-before
|
|
} else if name.try_match("cursor") {
|
|
Style::Cursor(s.parse()?)
|
|
// direction
|
|
} else if name.try_match("display") {
|
|
Style::Display(s.parse()?)
|
|
// elevation
|
|
// empty-cells
|
|
// flex
|
|
} else if name.try_match("flex-basis") {
|
|
Style::FlexBasis(s.parse()?)
|
|
} else if name.try_match("flex-direction") {
|
|
Style::FlexDirection(s.parse()?)
|
|
// flex-flow
|
|
} else if name.try_match("flex-grow") {
|
|
let number: Number = s.parse()?;
|
|
if !number.suffix.is_empty() {
|
|
return Err(syn::Error::new(number.span, "expected number"));
|
|
}
|
|
Style::FlexGrow(number.value)
|
|
} else if name.try_match("flex-shrink") {
|
|
let number: Number = s.parse()?;
|
|
if !number.suffix.is_empty() {
|
|
return Err(syn::Error::new(number.span, "expected number"));
|
|
}
|
|
Style::FlexShrink(number.value)
|
|
} else if name.try_match("flex-wrap") {
|
|
Style::FlexWrap(s.parse()?)
|
|
} else if name.try_match("float") {
|
|
Style::Float(s.parse()?)
|
|
// font
|
|
} else if name.try_match("font-family") {
|
|
Style::FontFamily(s.parse()?)
|
|
// font-feature-settings
|
|
// font-kerning
|
|
} else if name.try_match("font-size") {
|
|
Style::FontSize(s.parse()?)
|
|
// font-size-adjust
|
|
// font-stretch
|
|
} else if name.try_match("font-style") {
|
|
Style::FontStyle(s.parse()?)
|
|
// font-synthesis
|
|
// font-variant
|
|
// font-variant-caps
|
|
// font-variant-east-asian
|
|
// font-variant-ligatures
|
|
// font-variant-numeric
|
|
// font-variant-position
|
|
} else if name.try_match("font-weight") {
|
|
Style::FontWeight(s.parse()?)
|
|
// glyph-orientation-vertical
|
|
// grid
|
|
// grid-area
|
|
// grid-auto-columns
|
|
// grid-auto-flow
|
|
// grid-auto-rows
|
|
// grid-column
|
|
// grid-column-end
|
|
// grid-column-start
|
|
// grid-row
|
|
// grid-row-end
|
|
// grid-row-start
|
|
// grid-template
|
|
// grid-template-areas
|
|
// grid-template-columns
|
|
// grid-template-rows
|
|
} else if name.try_match("height") {
|
|
Style::Height(s.parse()?)
|
|
// image-orientation
|
|
// image-rendering
|
|
// isolation
|
|
} else if name.try_match("justify-content") {
|
|
Style::JustifyContent(s.parse()?)
|
|
} else if name.try_match("left") {
|
|
Style::Left(s.parse()?)
|
|
// letter-spacing
|
|
} else if name.try_match("line-height") {
|
|
Style::LineHeight(s.parse()?)
|
|
// list-style
|
|
// list-style-image
|
|
// list-style-position
|
|
} else if name.try_match("list-style-type") {
|
|
Style::ListStyleType(s.parse()?)
|
|
} else if name.try_match("margin") {
|
|
Style::Margin(s.parse()?)
|
|
} else if name.try_match("margin-bottom") {
|
|
Style::MarginBottom(s.parse()?)
|
|
} else if name.try_match("margin-left") {
|
|
Style::MarginLeft(s.parse()?)
|
|
} else if name.try_match("margin-right") {
|
|
Style::MarginRight(s.parse()?)
|
|
} else if name.try_match("margin-top") {
|
|
Style::MarginTop(s.parse()?)
|
|
// mask
|
|
// mask-border
|
|
// mask-border-mode
|
|
// mask-border-outset
|
|
// mask-border-repeat
|
|
// mask-border-slice
|
|
// mask-border-source
|
|
// mask-border-width
|
|
// mask-clip
|
|
// mask-composite
|
|
// mask-image
|
|
// mask-mode
|
|
// mask-origin
|
|
// mask-position
|
|
// mask-repeat
|
|
// mask-size
|
|
// mask-type
|
|
} else if name.try_match("max-height") {
|
|
Style::MaxHeight(s.parse()?)
|
|
} else if name.try_match("max-width") {
|
|
Style::MaxWidth(s.parse()?)
|
|
} else if name.try_match("min-height") {
|
|
Style::MinHeight(s.parse()?)
|
|
} else if name.try_match("min-width") {
|
|
Style::MinWidth(s.parse()?)
|
|
// mix-blend-mode
|
|
} else if name.try_match("object-fit") {
|
|
Style::ObjectFit(s.parse()?)
|
|
// object-position
|
|
// opacity
|
|
// order
|
|
// orphans
|
|
// outline
|
|
// outline-color
|
|
// outline-offset
|
|
// outline-style
|
|
// outline-width
|
|
} else if name.try_match("overflow") {
|
|
Style::Overflow(s.parse()?)
|
|
} else if name.try_match("overflow-x") {
|
|
Style::OverflowX(s.parse()?)
|
|
} else if name.try_match("overflow-y") {
|
|
Style::OverflowY(s.parse()?)
|
|
} else if name.try_match("padding") {
|
|
Style::Padding(s.parse()?)
|
|
} else if name.try_match("padding-bottom") {
|
|
Style::PaddingBottom(s.parse()?)
|
|
} else if name.try_match("padding-left") {
|
|
Style::PaddingLeft(s.parse()?)
|
|
} else if name.try_match("padding-right") {
|
|
Style::PaddingRight(s.parse()?)
|
|
} else if name.try_match("padding-top") {
|
|
Style::PaddingTop(s.parse()?)
|
|
// page-break-after
|
|
// page-break-before
|
|
// page-break-inside
|
|
// pause
|
|
// pause-after
|
|
// pause-before
|
|
// pitch
|
|
// pitch-range
|
|
// play-during
|
|
} else if name.try_match("position") {
|
|
Style::Position(s.parse()?)
|
|
// quotes
|
|
} else if name.try_match("resize") {
|
|
Style::Resize(s.parse()?)
|
|
// richness
|
|
} else if name.try_match("right") {
|
|
Style::Right(s.parse()?)
|
|
// scroll-margin
|
|
// scroll-margin-block
|
|
// scroll-margin-block-end
|
|
// scroll-margin-block-start
|
|
// scroll-margin-bottom
|
|
// scroll-margin-inline
|
|
// scroll-margin-inline-end
|
|
// scroll-margin-inline-start
|
|
// scroll-margin-left
|
|
// scroll-margin-right
|
|
// scroll-margin-top
|
|
// scroll-padding
|
|
// scroll-padding-block
|
|
// scroll-padding-block-end
|
|
// scroll-padding-block-start
|
|
// scroll-padding-bottom
|
|
// scroll-padding-inline
|
|
// scroll-padding-inline-end
|
|
// scroll-padding-inline-start
|
|
// scroll-padding-left
|
|
// scroll-padding-right
|
|
// scroll-padding-top
|
|
// scroll-snap-align
|
|
// scroll-snap-stop
|
|
// scroll-snap-type
|
|
// shape-image-threshold
|
|
// shape-margin
|
|
// shape-outside
|
|
// speak
|
|
// speak-header
|
|
// speak-numeral
|
|
// speak-punctuation
|
|
// speech-rate
|
|
// stress
|
|
// table-layout
|
|
} else if name.try_match("text-align") {
|
|
Style::TextAlign(s.parse()?)
|
|
// text-combine-upright
|
|
// text-decoration
|
|
// text-decoration-color
|
|
// text-decoration-line
|
|
// text-decoration-style
|
|
// text-emphasis
|
|
// text-emphasis-color
|
|
// text-emphasis-position
|
|
// text-emphasis-style
|
|
// text-indent
|
|
// text-orientation
|
|
// text-overflow
|
|
// text-shadow
|
|
// text-transform
|
|
// text-underline-position
|
|
} else if name.try_match("top") {
|
|
Style::Top(s.parse()?)
|
|
// transform
|
|
// transform-box
|
|
// transform-origin
|
|
// unicode-bidi
|
|
// vertical-align
|
|
// visibility
|
|
// voice-family
|
|
// volume
|
|
} else if name.try_match("white-space") {
|
|
Style::WhiteSpace(s.parse()?)
|
|
} else if name.try_match("widows") {
|
|
Style::Widows(integer(s, 1..)?)
|
|
} else if name.try_match("width") {
|
|
Style::Width(s.parse()?)
|
|
// will-change
|
|
// word-spacing
|
|
// writing-mode
|
|
// z-index
|
|
} else {
|
|
return Err(name.error());
|
|
};
|
|
|
|
if !finished_rule(s) {
|
|
return Err(s.error("unexpected trailing tokens in style rule"));
|
|
}
|
|
|
|
Ok(output)
|
|
}
|
|
}
|
|
|
|
impl Parse for AlignContent {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let name: HyphenWord = s.parse()?;
|
|
|
|
if name.try_match("flex-start") {
|
|
Ok(AlignContent::FlexStart)
|
|
} else if name.try_match("flex-end") {
|
|
Ok(AlignContent::FlexEnd)
|
|
} else if name.try_match("center") {
|
|
Ok(AlignContent::Center)
|
|
} else if name.try_match("space-between") {
|
|
Ok(AlignContent::SpaceBetween)
|
|
} else if name.try_match("space-around") {
|
|
Ok(AlignContent::SpaceAround)
|
|
} else if name.try_match("stretch") {
|
|
Ok(AlignContent::Stretch)
|
|
} else {
|
|
Err(name.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_align_content() {
|
|
for test in vec![
|
|
"flex-start",
|
|
"flex-end",
|
|
"center",
|
|
"space-between",
|
|
"space-around",
|
|
"stretch",
|
|
] {
|
|
assert_eq!(
|
|
&syn::parse_str::<AlignContent>(test).unwrap().to_string(),
|
|
test
|
|
);
|
|
}
|
|
assert_eq!(
|
|
&syn::parse_str::<Style>("align-content:flex-start")
|
|
.unwrap()
|
|
.to_string(),
|
|
"align-content:flex-start"
|
|
);
|
|
}
|
|
|
|
impl Parse for AlignItems {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("normal") {
|
|
Ok(AlignItems::Normal)
|
|
} else if word.try_match("stretch") {
|
|
Ok(AlignItems::Stretch)
|
|
} else if word.try_match("center") {
|
|
Ok(AlignItems::Center)
|
|
} else if word.try_match("start") {
|
|
Ok(AlignItems::Start)
|
|
} else if word.try_match("end") {
|
|
Ok(AlignItems::End)
|
|
} else if word.try_match("flex-start") {
|
|
Ok(AlignItems::FlexStart)
|
|
} else if word.try_match("flex-end") {
|
|
Ok(AlignItems::FlexEnd)
|
|
} else if word.try_match("baseline") {
|
|
Ok(AlignItems::Baseline)
|
|
} else if word.try_match("first") {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("baseline") {
|
|
Ok(AlignItems::FirstBaseline)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
} else if word.try_match("last") {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("baseline") {
|
|
Ok(AlignItems::LastBaseline)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
} else if word.try_match("safe") {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("center") {
|
|
Ok(AlignItems::SafeCenter)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
} else if word.try_match("unsafe") {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("center") {
|
|
Ok(AlignItems::UnsafeCenter)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for AlignSelf {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("auto") {
|
|
Ok(AlignSelf::Auto)
|
|
} else if word.try_match("normal") {
|
|
Ok(AlignSelf::Normal)
|
|
} else if word.try_match("center") {
|
|
Ok(AlignSelf::Center)
|
|
} else if word.try_match("start") {
|
|
Ok(AlignSelf::Start)
|
|
} else if word.try_match("self-start") {
|
|
Ok(AlignSelf::SelfStart)
|
|
} else if word.try_match("self-end") {
|
|
Ok(AlignSelf::SelfEnd)
|
|
} else if word.try_match("flex-start") {
|
|
Ok(AlignSelf::FlexStart)
|
|
} else if word.try_match("flex-end") {
|
|
Ok(AlignSelf::FlexEnd)
|
|
} else if word.try_match("baseline") {
|
|
Ok(AlignSelf::Baseline)
|
|
} else if word.try_match("first") {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("baseline") {
|
|
Ok(AlignSelf::FirstBaseline)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
} else if word.try_match("last") {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("baseline") {
|
|
Ok(AlignSelf::LastBaseline)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
} else if word.try_match("stretch") {
|
|
Ok(AlignSelf::Stretch)
|
|
} else if word.try_match("safe") {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("center") {
|
|
Ok(AlignSelf::SafeCenter)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
} else if word.try_match("unsafe") {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("center") {
|
|
Ok(AlignSelf::UnsafeCenter)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for BackgroundAttachment {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("scroll") {
|
|
Ok(BackgroundAttachment::Scroll)
|
|
} else if word.try_match("fixed") {
|
|
Ok(BackgroundAttachment::Fixed)
|
|
} else if word.try_match("local") {
|
|
Ok(BackgroundAttachment::Local)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for BlendMode {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("normal") {
|
|
Ok(BlendMode::Normal)
|
|
} else if word.try_match("multiply") {
|
|
Ok(BlendMode::Multiply)
|
|
} else if word.try_match("screen") {
|
|
Ok(BlendMode::Screen)
|
|
} else if word.try_match("overlay") {
|
|
Ok(BlendMode::Overlay)
|
|
} else if word.try_match("darken") {
|
|
Ok(BlendMode::Darken)
|
|
} else if word.try_match("lighten") {
|
|
Ok(BlendMode::Lighten)
|
|
} else if word.try_match("color-dodge") {
|
|
Ok(BlendMode::ColorDodge)
|
|
} else if word.try_match("color-burn") {
|
|
Ok(BlendMode::ColorBurn)
|
|
} else if word.try_match("hard-light") {
|
|
Ok(BlendMode::HardLight)
|
|
} else if word.try_match("soft-light") {
|
|
Ok(BlendMode::SoftLight)
|
|
} else if word.try_match("difference") {
|
|
Ok(BlendMode::Difference)
|
|
} else if word.try_match("exclusion") {
|
|
Ok(BlendMode::Exclusion)
|
|
} else if word.try_match("hue") {
|
|
Ok(BlendMode::Hue)
|
|
} else if word.try_match("saturation") {
|
|
Ok(BlendMode::Saturation)
|
|
} else if word.try_match("color") {
|
|
Ok(BlendMode::Color)
|
|
} else if word.try_match("luminosity") {
|
|
Ok(BlendMode::Luminosity)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for BackgroundImage {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let peek = HyphenWord::peek_specific(s);
|
|
if peek.as_ref().map(|s| s.as_str()) == Some("url") {
|
|
let url;
|
|
syn::parenthesized!(url in s);
|
|
let url = url.parse::<syn::LitStr>()?;
|
|
Ok(BackgroundImage::Url(url.value()))
|
|
} else {
|
|
let word: HyphenWord = s.parse()?;
|
|
word.add_expected("url");
|
|
if word.try_match("none") {
|
|
Ok(BackgroundImage::None)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for BackgroundBox {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("border-box") {
|
|
Ok(BackgroundBox::BorderBox)
|
|
} else if word.try_match("padding-box") {
|
|
Ok(BackgroundBox::PaddingBox)
|
|
} else if word.try_match("content-box") {
|
|
Ok(BackgroundBox::ContentBox)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for BackgroundPosition {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("top") {
|
|
Ok(BackgroundPosition::Top)
|
|
} else if word.try_match("bottom") {
|
|
Ok(BackgroundPosition::Bottom)
|
|
} else if word.try_match("left") {
|
|
Ok(BackgroundPosition::Left)
|
|
} else if word.try_match("right") {
|
|
Ok(BackgroundPosition::Right)
|
|
} else if word.try_match("center") {
|
|
Ok(BackgroundPosition::Center)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for BackgroundRepeat {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("repeat-x") {
|
|
Ok(BackgroundRepeat::RepeatX)
|
|
} else if word.try_match("repeat-y") {
|
|
Ok(BackgroundRepeat::RepeatY)
|
|
} else if let Ok(v) = s.parse() {
|
|
Ok(BackgroundRepeat::SingleOrDouble(v))
|
|
} else {
|
|
word.add_expected("repeat");
|
|
word.add_expected("space");
|
|
word.add_expected("round");
|
|
word.add_expected("no-repeat");
|
|
Err(word.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for BgRepeatPart {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("repeat") {
|
|
Ok(BgRepeatPart::Repeat)
|
|
} else if word.try_match("space") {
|
|
Ok(BgRepeatPart::Space)
|
|
} else if word.try_match("round") {
|
|
Ok(BgRepeatPart::Round)
|
|
} else if word.try_match("no-repeat") {
|
|
Ok(BgRepeatPart::NoRepeat)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for BackgroundSize {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("cover") {
|
|
Ok(BackgroundSize::Cover)
|
|
} else if word.try_match("contain") {
|
|
Ok(BackgroundSize::Contain)
|
|
} else if let Ok(v) = s.parse() {
|
|
Ok(BackgroundSize::SingleOrDouble(v))
|
|
} else {
|
|
word.add_expected("<length>");
|
|
word.add_expected("<percentage>");
|
|
word.add_expected("auto");
|
|
Err(word.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for Border {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
fn line_width_error(span: Span) -> syn::Error {
|
|
syn::Error::new(span, "the border width was specified more than once")
|
|
}
|
|
fn line_style_error(span: Span) -> syn::Error {
|
|
syn::Error::new(span, "the border style was specified more than once")
|
|
}
|
|
fn color_error(span: Span) -> syn::Error {
|
|
syn::Error::new(span, "the border color was specified more than once")
|
|
}
|
|
let mut border = Border::new();
|
|
while !(border.is_full() || finished_rule(s)) {
|
|
let mut matched_something = false; // prevents an infinite loop when no matches
|
|
let width_fork = s.fork();
|
|
match width_fork.parse::<LineWidth>() {
|
|
Ok(line_width) => {
|
|
if border.has_line_width() {
|
|
return Err(line_width_error(width_fork.cursor().span()));
|
|
}
|
|
matched_something = true;
|
|
border.line_width = Some(line_width);
|
|
s.advance_to(&width_fork);
|
|
}
|
|
Err(_) => (),
|
|
}
|
|
let style_fork = s.fork();
|
|
match style_fork.parse::<LineStyle>() {
|
|
Ok(line_style) => {
|
|
if border.has_line_style() {
|
|
return Err(line_style_error(style_fork.cursor().span()));
|
|
}
|
|
matched_something = true;
|
|
border.line_style = Some(line_style);
|
|
s.advance_to(&style_fork);
|
|
}
|
|
Err(_) => (),
|
|
}
|
|
let color_fork = s.fork();
|
|
match color_fork.parse::<Color>() {
|
|
Ok(color) => {
|
|
if border.has_color() {
|
|
return Err(color_error(color_fork.cursor().span()));
|
|
}
|
|
matched_something = true;
|
|
border.color = Some(color);
|
|
s.advance_to(&color_fork);
|
|
}
|
|
Err(_) => (),
|
|
}
|
|
if !(matched_something || finished_rule(s)) {
|
|
return Err(syn::Error::new(
|
|
s.cursor().span(),
|
|
"unexpected input - expected one of border-width, border-style, color",
|
|
));
|
|
}
|
|
}
|
|
Ok(border)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_border_color() {
|
|
for (input, output) in vec![
|
|
("black", Rect::All(Color::Black)),
|
|
(
|
|
"#fff blue",
|
|
Rect::VerticalHorizontal(Color::HexRGB(255, 255, 255), Color::Blue),
|
|
),
|
|
(
|
|
"blue hsl(20, 5%, 100%) white",
|
|
Rect::TopHorizontalBottom(Color::Blue, Color::HSL(20.0, 5.0, 100.0), Color::White),
|
|
),
|
|
(
|
|
"hsla(20, 5%, 100%, 0.2) #fff #ccc white",
|
|
Rect::TopRightBottomLeft(
|
|
Color::HSLA(20.0, 5.0, 100.0, 0.2),
|
|
Color::HexRGB(255, 255, 255),
|
|
Color::HexRGB(204, 204, 204),
|
|
Color::White,
|
|
),
|
|
),
|
|
] {
|
|
assert_eq!(syn::parse_str::<Rect<Color>>(input).unwrap(), output);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_border_width() {
|
|
for (input, output) in vec![
|
|
("1px", BorderWidth::All(LineWidth::Length(Length::Px(1.0)))),
|
|
(
|
|
"1px 2\"em\"",
|
|
BorderWidth::VerticalHorizontal(
|
|
LineWidth::Length(Length::Px(1.0)),
|
|
LineWidth::Length(Length::Em(2.0)),
|
|
),
|
|
),
|
|
(
|
|
"2\"em\" medium thick",
|
|
BorderWidth::TopHorizontalBottom(
|
|
LineWidth::Length(Length::Em(2.0)),
|
|
LineWidth::Medium,
|
|
LineWidth::Thick,
|
|
),
|
|
),
|
|
(
|
|
"2\"em\" medium 1px thick",
|
|
BorderWidth::TopRightBottomLeft(
|
|
LineWidth::Length(Length::Em(2.0)),
|
|
LineWidth::Medium,
|
|
LineWidth::Length(Length::Px(1.0)),
|
|
LineWidth::Thick,
|
|
),
|
|
),
|
|
] {
|
|
assert_eq!(syn::parse_str::<BorderWidth>(input).unwrap(), output);
|
|
}
|
|
|
|
for input in vec!["thi", "1px 1px 1px 1px 1px"] {
|
|
assert!(syn::parse_str::<BorderWidth>(input).is_err());
|
|
}
|
|
}
|
|
|
|
impl Parse for BorderCollapse {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("collapse") {
|
|
Ok(BorderCollapse::Collapse)
|
|
} else if word.try_match("separate") {
|
|
Ok(BorderCollapse::Separate)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for BoxShadow {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
syn::custom_keyword!(none);
|
|
if s.peek(none) {
|
|
s.parse::<none>()?;
|
|
Ok(BoxShadow::None)
|
|
} else {
|
|
Ok(BoxShadow::Shadows(s.parse()?))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for BoxSizing {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("border-box") {
|
|
Ok(BoxSizing::BorderBox)
|
|
} else if word.try_match("content-box") {
|
|
Ok(BoxSizing::ContentBox)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for Clear {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("none") {
|
|
Ok(Clear::None)
|
|
} else if word.try_match("left") {
|
|
Ok(Clear::Left)
|
|
} else if word.try_match("right") {
|
|
Ok(Clear::Right)
|
|
} else if word.try_match("both") {
|
|
Ok(Clear::Both)
|
|
} else if word.try_match("inline-start") {
|
|
Ok(Clear::InlineStart)
|
|
} else if word.try_match("inline-end") {
|
|
Ok(Clear::InlineEnd)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for ColumnCount {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
if s.peek(syn::LitInt) {
|
|
Ok(ColumnCount::Fixed(s.parse::<Integer<u32>>()?.into_inner()))
|
|
} else {
|
|
let word: HyphenWord = s.parse()?;
|
|
word.add_expected("integer");
|
|
if word.try_match("auto") {
|
|
Ok(ColumnCount::Auto)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_clear() {
|
|
for (input, output) in vec![
|
|
("none", Clear::None),
|
|
("left", Clear::Left),
|
|
("right", Clear::Right),
|
|
("both", Clear::Both),
|
|
("inline-start", Clear::InlineStart),
|
|
("inline-end", Clear::InlineEnd),
|
|
] {
|
|
assert_eq!(syn::parse_str::<Clear>(input).unwrap(), output);
|
|
}
|
|
}
|
|
|
|
impl Parse for Cursor {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("auto") {
|
|
Ok(Cursor::Auto)
|
|
} else if word.try_match("default") {
|
|
Ok(Cursor::Default)
|
|
} else if word.try_match("none") {
|
|
Ok(Cursor::None)
|
|
} else if word.try_match("context-menu") {
|
|
Ok(Cursor::ContextMenu)
|
|
} else if word.try_match("help") {
|
|
Ok(Cursor::Help)
|
|
} else if word.try_match("pointer") {
|
|
Ok(Cursor::Pointer)
|
|
} else if word.try_match("progress") {
|
|
Ok(Cursor::Progress)
|
|
} else if word.try_match("wait") {
|
|
Ok(Cursor::Wait)
|
|
} else if word.try_match("cell") {
|
|
Ok(Cursor::Cell)
|
|
} else if word.try_match("crosshair") {
|
|
Ok(Cursor::Crosshair)
|
|
} else if word.try_match("text") {
|
|
Ok(Cursor::Text)
|
|
} else if word.try_match("vertical-text") {
|
|
Ok(Cursor::VerticalText)
|
|
} else if word.try_match("alias") {
|
|
Ok(Cursor::Alias)
|
|
} else if word.try_match("copy") {
|
|
Ok(Cursor::Copy)
|
|
} else if word.try_match("move") {
|
|
Ok(Cursor::Move)
|
|
} else if word.try_match("no-drop") {
|
|
Ok(Cursor::NoDrop)
|
|
} else if word.try_match("not-allowed") {
|
|
Ok(Cursor::NotAllowed)
|
|
} else if word.try_match("grab") {
|
|
Ok(Cursor::Grab)
|
|
} else if word.try_match("grabbing") {
|
|
Ok(Cursor::Grabbing)
|
|
} else if word.try_match("e-resize") {
|
|
Ok(Cursor::EResize)
|
|
} else if word.try_match("n-resize") {
|
|
Ok(Cursor::NResize)
|
|
} else if word.try_match("ne-resize") {
|
|
Ok(Cursor::NEResize)
|
|
} else if word.try_match("nw-resize") {
|
|
Ok(Cursor::NWResize)
|
|
} else if word.try_match("s-resize") {
|
|
Ok(Cursor::SResize)
|
|
} else if word.try_match("se-resize") {
|
|
Ok(Cursor::SEResize)
|
|
} else if word.try_match("sw-resize") {
|
|
Ok(Cursor::SWResize)
|
|
} else if word.try_match("w-resize") {
|
|
Ok(Cursor::WResize)
|
|
} else if word.try_match("ew-resize") {
|
|
Ok(Cursor::EWResize)
|
|
} else if word.try_match("ns-resize") {
|
|
Ok(Cursor::NSResize)
|
|
} else if word.try_match("nesw-resize") {
|
|
Ok(Cursor::NESWResize)
|
|
} else if word.try_match("nwse-resize") {
|
|
Ok(Cursor::NWSEResize)
|
|
} else if word.try_match("col-resize") {
|
|
Ok(Cursor::ColResize)
|
|
} else if word.try_match("row-resize") {
|
|
Ok(Cursor::RowResize)
|
|
} else if word.try_match("all-scroll") {
|
|
Ok(Cursor::AllScroll)
|
|
} else if word.try_match("zoom-in") {
|
|
Ok(Cursor::ZoomIn)
|
|
} else if word.try_match("zoom-out") {
|
|
Ok(Cursor::ZoomOut)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for Display {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("block") {
|
|
Ok(Display::Block)
|
|
} else if word.try_match("flex") {
|
|
Ok(Display::Flex)
|
|
} else if word.try_match("inline") {
|
|
Ok(Display::Inline)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for FlexBasis {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
syn::custom_keyword!(content);
|
|
|
|
if s.peek(content) {
|
|
s.parse::<content>()?;
|
|
Ok(FlexBasis::Content)
|
|
} else {
|
|
let w: Width21 = s.parse()?;
|
|
Ok(FlexBasis::Width(w))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for FlexDirection {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("column") {
|
|
Ok(FlexDirection::Column)
|
|
} else if word.try_match("row") {
|
|
Ok(FlexDirection::Row)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for FlexWrap {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("wrap") {
|
|
Ok(FlexWrap::Wrap)
|
|
} else if word.try_match("nowrap") {
|
|
Ok(FlexWrap::Nowrap)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for Float {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("none") {
|
|
Ok(Float::None)
|
|
} else if word.try_match("left") {
|
|
Ok(Float::Left)
|
|
} else if word.try_match("right") {
|
|
Ok(Float::Right)
|
|
} else if word.try_match("inline-start") {
|
|
Ok(Float::InlineStart)
|
|
} else if word.try_match("inline-end") {
|
|
Ok(Float::InlineEnd)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for Font {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
if s.peek(syn::LitStr) {
|
|
Ok(Font::Named(s.parse::<syn::LitStr>()?.value()))
|
|
} else {
|
|
let name: HyphenWord = s.parse()?;
|
|
name.add_expected("named font");
|
|
|
|
if name.try_match("serif") {
|
|
Ok(Font::Serif)
|
|
} else if name.try_match("sans-serif") {
|
|
Ok(Font::SansSerif)
|
|
} else if name.try_match("cursive") {
|
|
Ok(Font::Cursive)
|
|
} else if name.try_match("fantasy") {
|
|
Ok(Font::Fantasy)
|
|
} else if name.try_match("monospace") {
|
|
Ok(Font::Fantasy)
|
|
} else {
|
|
Err(name.error())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_font_family() {
|
|
for (input, output) in vec![
|
|
(
|
|
"cursive",
|
|
FontFamily {
|
|
first: Font::Cursive,
|
|
rest: vec![],
|
|
},
|
|
),
|
|
(
|
|
"\"Amatic SC\", sans-serif",
|
|
FontFamily {
|
|
first: Font::Named("Amatic SC".to_string()),
|
|
rest: vec![Font::SansSerif],
|
|
},
|
|
),
|
|
] {
|
|
assert_eq!(syn::parse_str::<FontFamily>(input).unwrap(), output);
|
|
}
|
|
|
|
for val in vec![
|
|
"font-family:\"Font Awesome 5 Free\"",
|
|
"font-family:\"Some Name\",\"Another Name\",serif",
|
|
] {
|
|
assert_eq!(&syn::parse_str::<Style>(val).unwrap().to_string(), val);
|
|
}
|
|
}
|
|
|
|
impl Parse for FontSize {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let word_fork = s.fork();
|
|
let name: HyphenWord = word_fork.parse()?;
|
|
|
|
if name.try_match("xx-small") {
|
|
s.advance_to(&word_fork);
|
|
Ok(FontSize::XXSmall)
|
|
} else if name.try_match("x-small") {
|
|
s.advance_to(&word_fork);
|
|
Ok(FontSize::XSmall)
|
|
} else if name.try_match("small") {
|
|
s.advance_to(&word_fork);
|
|
Ok(FontSize::Small)
|
|
} else if name.try_match("medium") {
|
|
s.advance_to(&word_fork);
|
|
Ok(FontSize::Medium)
|
|
} else if name.try_match("large") {
|
|
s.advance_to(&word_fork);
|
|
Ok(FontSize::Large)
|
|
} else if name.try_match("x-large") {
|
|
s.advance_to(&word_fork);
|
|
Ok(FontSize::XLarge)
|
|
} else if name.try_match("xx-large") {
|
|
s.advance_to(&word_fork);
|
|
Ok(FontSize::XXLarge)
|
|
} else if name.try_match("xxx-large") {
|
|
s.advance_to(&word_fork);
|
|
Ok(FontSize::XXXLarge)
|
|
} else if name.try_match("larger") {
|
|
s.advance_to(&word_fork);
|
|
Ok(FontSize::Larger)
|
|
} else if name.try_match("smaller") {
|
|
s.advance_to(&word_fork);
|
|
Ok(FontSize::Smaller)
|
|
} else {
|
|
s.parse().map(FontSize::LengthPercentage).map_err(|_| {
|
|
name.add_expected("length");
|
|
name.add_expected("percentage");
|
|
name.error()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
impl Parse for FontStyle {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let name: HyphenWord = s.parse()?;
|
|
|
|
if name.try_match("normal") {
|
|
Ok(FontStyle::Normal)
|
|
} else if name.try_match("italic") {
|
|
Ok(FontStyle::Italic)
|
|
} else if name.try_match("oblique") {
|
|
Ok(FontStyle::Oblique)
|
|
} else {
|
|
Err(name.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_font_style() {
|
|
for (input, output) in vec![
|
|
("normal", FontStyle::Normal),
|
|
("italic", FontStyle::Italic),
|
|
("oblique", FontStyle::Oblique),
|
|
] {
|
|
assert_eq!(syn::parse_str::<FontStyle>(input).unwrap(), output);
|
|
}
|
|
|
|
for input in vec!["norma", "normal trailing"] {
|
|
assert!(syn::parse_str::<FontStyle>(input).is_err());
|
|
}
|
|
}
|
|
|
|
impl Parse for FontWeight {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let name: HyphenWord = s.parse()?;
|
|
name.add_expected("number where 1 <= number <= 1000");
|
|
|
|
if name.try_match("normal") {
|
|
Ok(FontWeight::Normal)
|
|
} else if name.try_match("bold") {
|
|
Ok(FontWeight::Bold)
|
|
} else if name.try_match("lighter") {
|
|
Ok(FontWeight::Lighter)
|
|
} else if name.try_match("bolder") {
|
|
Ok(FontWeight::Bolder)
|
|
} else {
|
|
let n: Number = s.parse().map_err(|_| name.error())?;
|
|
if n.suffix.is_empty() && n.value >= 1.0 && n.value <= 1000.0 {
|
|
Ok(FontWeight::Number(n.value))
|
|
} else {
|
|
Err(name.error())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_font_weight() {
|
|
for (input, output) in vec![
|
|
("normal", FontWeight::Normal),
|
|
("bold", FontWeight::Bold),
|
|
("lighter", FontWeight::Lighter),
|
|
("bolder", FontWeight::Bolder),
|
|
("1", FontWeight::Number(1.0)),
|
|
("1.0", FontWeight::Number(1.0)),
|
|
("1000", FontWeight::Number(1000.0)),
|
|
("1000.0", FontWeight::Number(1000.0)),
|
|
("246.15", FontWeight::Number(246.15)),
|
|
] {
|
|
match syn::parse_str::<FontWeight>(input) {
|
|
Ok(v) => assert_eq!(v, output),
|
|
Err(e) => panic!("error parsing {}: {}", input, e),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for JustifyContent {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let name: HyphenWord = s.parse()?;
|
|
|
|
if name.try_match("flex-start") {
|
|
Ok(JustifyContent::FlexStart)
|
|
} else if name.try_match("flex-end") {
|
|
Ok(JustifyContent::FlexEnd)
|
|
} else if name.try_match("center") {
|
|
Ok(JustifyContent::Center)
|
|
} else if name.try_match("space-between") {
|
|
Ok(JustifyContent::SpaceBetween)
|
|
} else if name.try_match("space-around") {
|
|
Ok(JustifyContent::SpaceAround)
|
|
} else if name.try_match("start") {
|
|
// - not in level 1 spec
|
|
Ok(JustifyContent::FlexStart)
|
|
} else if name.try_match("end") {
|
|
// - not in level 1 spec
|
|
Ok(JustifyContent::FlexEnd)
|
|
} else {
|
|
Err(name.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for Length {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let neg = if s.peek(Token![-]) {
|
|
s.parse::<Token![-]>()?;
|
|
true
|
|
} else {
|
|
false
|
|
};
|
|
let n: Number = s.parse()?;
|
|
Length::parse_from_number(n, neg)
|
|
}
|
|
}
|
|
|
|
impl Length {
|
|
fn parse_from_number(n: Number, neg: bool) -> syn::Result<Self> {
|
|
let neg = if neg { -1.0 } else { 1.0 };
|
|
if n.suffix == "em" {
|
|
Ok(Length::Em(n.value * neg))
|
|
} else if n.suffix == "ex" {
|
|
Ok(Length::Ex(n.value * neg))
|
|
} else if n.suffix == "in" {
|
|
Ok(Length::In(n.value * neg))
|
|
} else if n.suffix == "cm" {
|
|
Ok(Length::Cm(n.value * neg))
|
|
} else if n.suffix == "mm" {
|
|
Ok(Length::Mm(n.value * neg))
|
|
} else if n.suffix == "pt" {
|
|
Ok(Length::Pt(n.value * neg))
|
|
} else if n.suffix == "pc" {
|
|
Ok(Length::Pc(n.value * neg))
|
|
} else if n.suffix == "px" {
|
|
Ok(Length::Px(n.value * neg))
|
|
} else if n.suffix == "" && n.value == 0.0 {
|
|
Ok(Length::Zero)
|
|
} else {
|
|
// No matches so return error
|
|
Err(syn::Error::new(
|
|
n.span,
|
|
"expected one of `\"em\"`, `\"ex\"`, `in`, `cm`, `mm`, `pt`, `pc`, `px` after number, or 0",
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for LineStyle {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let name = s.parse::<HyphenWord>()?;
|
|
if name.try_match("none") {
|
|
Ok(LineStyle::None)
|
|
} else if name.try_match("hidden") {
|
|
Ok(LineStyle::Hidden)
|
|
} else if name.try_match("dotted") {
|
|
Ok(LineStyle::Dotted)
|
|
} else if name.try_match("dashed") {
|
|
Ok(LineStyle::Dashed)
|
|
} else if name.try_match("solid") {
|
|
Ok(LineStyle::Solid)
|
|
} else if name.try_match("double") {
|
|
Ok(LineStyle::Double)
|
|
} else if name.try_match("groove") {
|
|
Ok(LineStyle::Groove)
|
|
} else if name.try_match("ridge") {
|
|
Ok(LineStyle::Ridge)
|
|
} else if name.try_match("inset") {
|
|
Ok(LineStyle::Inset)
|
|
} else if name.try_match("outset") {
|
|
Ok(LineStyle::Outset)
|
|
} else {
|
|
Err(name.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for LineWidth {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let name = s.parse::<HyphenWord>()?;
|
|
if name.try_match("thin") {
|
|
Ok(LineWidth::Thin)
|
|
} else if name.try_match("medium") {
|
|
Ok(LineWidth::Medium)
|
|
} else if name.try_match("thick") {
|
|
Ok(LineWidth::Thick)
|
|
} else {
|
|
match s.parse::<Length>() {
|
|
Ok(l) => Ok(LineWidth::Length(l)),
|
|
Err(_) => {
|
|
name.add_expected("length");
|
|
Err(name.error())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_line_width() {
|
|
assert_eq!(
|
|
syn::parse_str::<LineWidth>("thin").unwrap(),
|
|
LineWidth::Thin
|
|
);
|
|
}
|
|
|
|
impl Parse for LineHeight {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
Ok(LineHeight(s.parse::<syn::LitFloat>()?.base10_parse()?))
|
|
}
|
|
}
|
|
|
|
impl Parse for ListStyleType {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let name: HyphenWord = s.parse()?;
|
|
|
|
if name.try_match("disc") {
|
|
Ok(ListStyleType::Disc)
|
|
} else if name.try_match("circle") {
|
|
Ok(ListStyleType::Circle)
|
|
} else if name.try_match("square") {
|
|
Ok(ListStyleType::Square)
|
|
} else if name.try_match("decimal") {
|
|
Ok(ListStyleType::Decimal)
|
|
} else if name.try_match("decimal-leading-zero") {
|
|
Ok(ListStyleType::DecimalLeadingZero)
|
|
} else if name.try_match("lower-roman") {
|
|
Ok(ListStyleType::LowerRoman)
|
|
} else if name.try_match("upper-roman") {
|
|
Ok(ListStyleType::UpperRoman)
|
|
} else if name.try_match("lower-greek") {
|
|
Ok(ListStyleType::LowerGreek)
|
|
} else if name.try_match("upper-greek") {
|
|
Ok(ListStyleType::UpperGreek)
|
|
} else if name.try_match("lower-latin") {
|
|
Ok(ListStyleType::LowerLatin)
|
|
} else if name.try_match("upper-latin") {
|
|
Ok(ListStyleType::UpperLatin)
|
|
} else if name.try_match("armenian") {
|
|
Ok(ListStyleType::Armenian)
|
|
} else if name.try_match("georgian") {
|
|
Ok(ListStyleType::Georgian)
|
|
} else if name.try_match("lower-alpha") {
|
|
Ok(ListStyleType::LowerAlpha)
|
|
} else if name.try_match("upper-alpha") {
|
|
Ok(ListStyleType::UpperAlpha)
|
|
} else if name.try_match("none") {
|
|
Ok(ListStyleType::None)
|
|
} else {
|
|
Err(name.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for MaxWidthHeight {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let name = s.parse::<HyphenWord>()?;
|
|
name.add_expected("length");
|
|
name.add_expected("percentage");
|
|
if name.try_match("none") {
|
|
Ok(MaxWidthHeight::None)
|
|
} else if name.try_match("min-content") {
|
|
Ok(MaxWidthHeight::MinContent)
|
|
} else if name.try_match("max-content") {
|
|
Ok(MaxWidthHeight::MaxContent)
|
|
} else if name.try_match("fit-content") {
|
|
let content;
|
|
syn::parenthesized!(content in s);
|
|
Ok(MaxWidthHeight::FitContent(content.parse()?))
|
|
} else {
|
|
s.parse()
|
|
.map(|lp| MaxWidthHeight::LengthPercentage(lp))
|
|
.map_err(|_| name.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_max_width_height() {
|
|
let style: Style = syn::parse_str("max-width: 200px").unwrap();
|
|
assert_eq!(&style.to_string(), "max-width:200px");
|
|
}
|
|
|
|
impl<T> Parse for Rect<T>
|
|
where
|
|
T: Parse,
|
|
{
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let first = s.parse::<T>()?;
|
|
let fork = s.fork();
|
|
let second = match fork.parse::<T>() {
|
|
Ok(v) => {
|
|
s.advance_to(&fork);
|
|
v
|
|
}
|
|
Err(_) => return Ok(Rect::All(first)),
|
|
};
|
|
let third = match fork.parse::<T>() {
|
|
Ok(v) => {
|
|
s.advance_to(&fork);
|
|
v
|
|
}
|
|
Err(_) => return Ok(Rect::VerticalHorizontal(first, second)),
|
|
};
|
|
match fork.parse::<T>() {
|
|
Ok(v) => {
|
|
s.advance_to(&fork);
|
|
Ok(Rect::TopRightBottomLeft(first, second, third, v))
|
|
}
|
|
Err(_) => Ok(Rect::TopHorizontalBottom(first, second, third)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for AutoLengthPercentage {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
syn::custom_keyword!(auto);
|
|
if s.peek(auto) {
|
|
s.parse::<auto>()?;
|
|
Ok(AutoLengthPercentage::Auto)
|
|
} else {
|
|
Ok(AutoLengthPercentage::LengthPercentage(s.parse()?))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for ObjectFit {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let name: HyphenWord = s.parse()?;
|
|
if name.try_match("fill") {
|
|
Ok(ObjectFit::Fill)
|
|
} else if name.try_match("none") {
|
|
Ok(ObjectFit::None)
|
|
} else if name.try_match("contain") {
|
|
if s.is_empty() {
|
|
Ok(ObjectFit::Contain { scale_down: false })
|
|
} else {
|
|
let scale_down_word: HyphenWord = s.parse()?;
|
|
if scale_down_word.try_match("scale-down") {
|
|
Ok(ObjectFit::Contain { scale_down: true })
|
|
} else {
|
|
Err(scale_down_word.error())
|
|
}
|
|
}
|
|
} else if name.try_match("cover") {
|
|
if HyphenWord::peek(s) {
|
|
let scale_down_word: HyphenWord = s.parse()?;
|
|
if scale_down_word.try_match("scale-down") {
|
|
Ok(ObjectFit::Cover { scale_down: true })
|
|
} else {
|
|
Err(scale_down_word.error())
|
|
}
|
|
} else {
|
|
Ok(ObjectFit::Cover { scale_down: false })
|
|
}
|
|
} else if name.try_match("scale-down") {
|
|
if HyphenWord::peek(s) {
|
|
let cover_contain: HyphenWord = s.parse()?;
|
|
if cover_contain.try_match("cover") {
|
|
Ok(ObjectFit::Cover { scale_down: true })
|
|
} else if cover_contain.try_match("contain") {
|
|
Ok(ObjectFit::Contain { scale_down: true })
|
|
} else {
|
|
Err(cover_contain.error())
|
|
}
|
|
} else {
|
|
// defaults to contain when cover/contain not present
|
|
Ok(ObjectFit::Contain { scale_down: true })
|
|
}
|
|
} else {
|
|
Err(name.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for Overflow {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let first = s.parse::<OverflowXY>()?;
|
|
Ok(match s.parse::<OverflowXY>() {
|
|
Ok(second) => Overflow::XY(first, second),
|
|
Err(_) => Overflow::Both(first),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Parse for OverflowXY {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let name: HyphenWord = s.parse()?;
|
|
|
|
if name.try_match("visible") {
|
|
Ok(OverflowXY::Visible)
|
|
} else if name.try_match("hidden") {
|
|
Ok(OverflowXY::Hidden)
|
|
} else if name.try_match("clip") {
|
|
Ok(OverflowXY::Clip)
|
|
} else if name.try_match("scroll") {
|
|
Ok(OverflowXY::Scroll)
|
|
} else if name.try_match("auto") {
|
|
Ok(OverflowXY::Auto)
|
|
} else {
|
|
Err(name.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for Position {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let name: HyphenWord = s.parse()?;
|
|
if name.try_match("static") {
|
|
Ok(Position::Static)
|
|
} else if name.try_match("relative") {
|
|
Ok(Position::Relative)
|
|
} else if name.try_match("absolute") {
|
|
Ok(Position::Absolute)
|
|
} else if name.try_match("fixed") {
|
|
Ok(Position::Fixed)
|
|
} else {
|
|
Err(name.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_padding() {
|
|
for (input, output) in vec![(
|
|
"padding:1\"em\"",
|
|
Style::Padding(Padding::All(Calc::Normal(LengthPercentage::Length(
|
|
Length::Em(1.0),
|
|
)))),
|
|
)] {
|
|
assert_eq!(syn::parse_str::<Style>(input).unwrap(), output);
|
|
}
|
|
}
|
|
|
|
impl Parse for Percentage {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let n: Number = s.parse()?;
|
|
if n.suffix == "%" {
|
|
Ok(Percentage(n.value))
|
|
} else {
|
|
Err(syn::Error::new(n.span, "expected percentage"))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for WhiteSpace {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let name: HyphenWord = s.parse()?;
|
|
if name.try_match("normal") {
|
|
Ok(WhiteSpace::Normal)
|
|
} else if name.try_match("pre") {
|
|
Ok(WhiteSpace::Pre)
|
|
} else if name.try_match("nowrap") {
|
|
Ok(WhiteSpace::Nowrap)
|
|
} else if name.try_match("pre-wrap") {
|
|
Ok(WhiteSpace::PreWrap)
|
|
} else if name.try_match("pre-line") {
|
|
Ok(WhiteSpace::PreLine)
|
|
} else {
|
|
Err(name.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for Width21 {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
syn::custom_keyword!(auto);
|
|
|
|
if s.peek(auto) {
|
|
s.parse::<auto>()?;
|
|
Ok(Width21::Auto)
|
|
} else {
|
|
Ok(Width21::LengthPercentage(s.parse()?))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for WidthHeight {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let fork = s.fork();
|
|
let name: HyphenWord = fork.parse()?;
|
|
|
|
if name.try_match("auto") {
|
|
s.advance_to(&fork);
|
|
Ok(WidthHeight::Auto)
|
|
} else if name.try_match("min-content") {
|
|
s.advance_to(&fork);
|
|
Ok(WidthHeight::MinContent)
|
|
} else if name.try_match("max-content") {
|
|
s.advance_to(&fork);
|
|
Ok(WidthHeight::MaxContent)
|
|
} else if name.try_match("fit-content") {
|
|
s.advance_to(&fork);
|
|
let content;
|
|
syn::parenthesized!(content in s);
|
|
let lp = content.parse()?;
|
|
if !content.is_empty() {
|
|
Err(content.error("trailing tokens"))
|
|
} else {
|
|
Ok(WidthHeight::FitContent(lp))
|
|
}
|
|
} else {
|
|
// todo error message
|
|
Ok(WidthHeight::LengthPercentage(s.parse()?))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_width_height() {
|
|
for (input, output) in vec![
|
|
("0", "0"),
|
|
("1px", "1px"),
|
|
("1\"em\"", "1em"),
|
|
("calc(100% - 60px)", "calc(100% - 60px)"),
|
|
] {
|
|
match syn::parse_str::<WidthHeight>(input) {
|
|
Ok(v) => assert_eq!(&v.to_string(), output),
|
|
Err(e) => panic!("Error in \"{}\": {}", input, e),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for LengthPercentage {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
if s.peek2(Token![%]) {
|
|
Ok(LengthPercentage::Percentage(s.parse()?))
|
|
} else {
|
|
Ok(LengthPercentage::Length(s.parse()?))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_length_percentage() {
|
|
for (input, output) in vec![
|
|
("1\"em\"", LengthPercentage::Length(Length::Em(1.0))),
|
|
("1.0px", LengthPercentage::Length(Length::Px(1.0))),
|
|
("0", LengthPercentage::Length(Length::Zero)),
|
|
] {
|
|
assert_eq!(syn::parse_str::<LengthPercentage>(input).unwrap(), output);
|
|
}
|
|
}
|
|
|
|
impl Parse for Resize {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let name: HyphenWord = s.parse()?;
|
|
|
|
if name.try_match("none") {
|
|
Ok(Resize::None)
|
|
} else if name.try_match("both") {
|
|
Ok(Resize::Both)
|
|
} else if name.try_match("horizontal") {
|
|
Ok(Resize::Horizontal)
|
|
} else if name.try_match("vertical") {
|
|
Ok(Resize::Vertical)
|
|
} else {
|
|
Err(name.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for Shadow {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
syn::custom_keyword!(inset);
|
|
let mut inset_val = false;
|
|
let mut length: Option<ShadowLength> = None;
|
|
let mut color: Option<Color> = None;
|
|
// keep trying all three until we're done or there is an error
|
|
loop {
|
|
let mut parsed_something = false;
|
|
// inset (easiest)
|
|
if s.peek(inset) {
|
|
let inset_tok = s.parse::<inset>()?;
|
|
if inset_val {
|
|
return Err(syn::Error::new(
|
|
inset_tok.span(),
|
|
"`inset` must be specified 0 or 1 times",
|
|
));
|
|
}
|
|
inset_val = true;
|
|
parsed_something = true;
|
|
}
|
|
|
|
// color
|
|
let fork = s.fork();
|
|
if let Ok(parsed_color) = fork.parse::<Color>() {
|
|
if color.is_some() {
|
|
return Err(s.error("color must be specified 0 or 1 times"));
|
|
}
|
|
color = Some(parsed_color);
|
|
s.advance_to(&fork);
|
|
parsed_something = true;
|
|
}
|
|
|
|
// length
|
|
let fork = s.fork();
|
|
if let Ok(parsed_length) = fork.parse::<ShadowLength>() {
|
|
if length.is_some() {
|
|
return Err(s.error("shadow length must be specified once"));
|
|
}
|
|
length = Some(parsed_length);
|
|
s.advance_to(&fork);
|
|
parsed_something = true;
|
|
}
|
|
|
|
// if we've failed to parse anything, end the loop.
|
|
if !parsed_something {
|
|
break;
|
|
}
|
|
}
|
|
if let Some(length) = length {
|
|
Ok(Shadow {
|
|
color,
|
|
length,
|
|
inset: inset_val,
|
|
})
|
|
} else {
|
|
Err(s.error("expected color, length, or `inset`"))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for ShadowLength {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let horizontal: Length = s.parse()?;
|
|
let vertical: Length = s.parse()?;
|
|
|
|
// blur
|
|
let fork = s.fork();
|
|
let blur = match fork.parse::<Length>() {
|
|
Ok(blur) => {
|
|
s.advance_to(&fork);
|
|
blur
|
|
}
|
|
Err(_) => {
|
|
return Ok(ShadowLength::Offsets {
|
|
horizontal,
|
|
vertical,
|
|
});
|
|
}
|
|
};
|
|
|
|
// spread
|
|
let fork = s.fork();
|
|
match fork.parse::<Length>() {
|
|
Ok(spread) => {
|
|
s.advance_to(&fork);
|
|
|
|
Ok(ShadowLength::OffsetsBlurSpread {
|
|
horizontal,
|
|
vertical,
|
|
blur,
|
|
spread,
|
|
})
|
|
}
|
|
Err(_) => Ok(ShadowLength::OffsetsBlur {
|
|
horizontal,
|
|
vertical,
|
|
blur,
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for TextAlign {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let word: HyphenWord = s.parse()?;
|
|
if word.try_match("left") {
|
|
Ok(TextAlign::Left)
|
|
} else if word.try_match("right") {
|
|
Ok(TextAlign::Right)
|
|
} else if word.try_match("center") {
|
|
Ok(TextAlign::Center)
|
|
} else if word.try_match("justify") {
|
|
Ok(TextAlign::Justify)
|
|
} else {
|
|
Err(word.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
// color
|
|
// =====
|
|
|
|
impl Parse for DynamicColor {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
Ok(if s.peek(syn::token::Brace) {
|
|
DynamicColor::Dynamic(s.parse()?)
|
|
} else {
|
|
DynamicColor::Literal(s.parse()?)
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Parse for Color {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
if s.peek(Token![#]) {
|
|
return parse_hex_color(s);
|
|
}
|
|
let fn_name: HyphenWord = s.parse()?;
|
|
if fn_name.try_match("hsl") {
|
|
parse_hsl_color(s, false)
|
|
} else if fn_name.try_match("hsla") {
|
|
parse_hsl_color(s, true)
|
|
} else {
|
|
if let Some(name) = fn_name.word.as_ref() {
|
|
if let Some(color) = Color::from_named(name) {
|
|
return Ok(color);
|
|
}
|
|
}
|
|
fn_name.add_expected("named color");
|
|
Err(fn_name.error())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn parse_hex_color(s: ParseStream) -> syn::Result<Color> {
|
|
const ERR_MSG: &'static str = "to avoid confusing rust, please enclose hex colors in `\"`";
|
|
s.parse::<Token![#]>()?;
|
|
if !(s.peek(syn::LitStr) || s.peek(Ident)) {
|
|
return Err(s.error(ERR_MSG));
|
|
}
|
|
if s.peek(syn::LitStr) {
|
|
let hex_str: syn::LitStr = s.parse()?;
|
|
color::parse_hex(&hex_str.value()).ok_or(syn::Error::new(hex_str.span(), ERR_MSG))
|
|
} else {
|
|
let hex_str: Ident = s.parse()?;
|
|
color::parse_hex(&hex_str.to_string()).ok_or(syn::Error::new(hex_str.span(), ERR_MSG))
|
|
}
|
|
}
|
|
|
|
fn parse_hsl_color(s: ParseStream, with_alpha: bool) -> syn::Result<Color> {
|
|
let content;
|
|
syn::parenthesized!(content in s);
|
|
let n: Number = content.parse()?;
|
|
n.empty_suffix()?;
|
|
let hue = n.value;
|
|
if hue < 0.0 || hue >= 360.0 {
|
|
return Err(syn::Error::new(
|
|
n.span,
|
|
"hue should be in the range `0 <= hue < 360`",
|
|
));
|
|
}
|
|
content.parse::<Token![,]>()?;
|
|
let n: Number = content.parse()?;
|
|
if n.suffix != "%" {
|
|
return Err(syn::Error::new(
|
|
n.span,
|
|
"saturation should be a percentage (followed by `%`)",
|
|
));
|
|
}
|
|
let sat = n.value;
|
|
if sat < 0.0 || sat > 100.0 {
|
|
return Err(syn::Error::new(
|
|
n.span,
|
|
"saturation should be in the range `0 <= sat < 100`",
|
|
));
|
|
}
|
|
content.parse::<Token![,]>()?;
|
|
let n: Number = content.parse()?;
|
|
if n.suffix != "%" {
|
|
return Err(syn::Error::new(
|
|
n.span,
|
|
"saturation should be a percentage (followed by `%`)",
|
|
));
|
|
}
|
|
let light = n.value;
|
|
if light < 0.0 || light > 100.0 {
|
|
return Err(syn::Error::new(
|
|
n.span,
|
|
"lightness should be in the range `0 <= light < 100`",
|
|
));
|
|
}
|
|
// since we parse content in parentheses, we can assume no trailing characers
|
|
if !with_alpha {
|
|
return if content.is_empty() {
|
|
Ok(Color::HSL(hue, sat, light))
|
|
} else {
|
|
Err(content.error("trailing characters"))
|
|
};
|
|
}
|
|
// we are a hsla
|
|
content.parse::<Token![,]>()?;
|
|
let n: Number = content.parse()?;
|
|
n.empty_suffix()?;
|
|
let alpha = n.value;
|
|
if alpha < 0.0 || alpha > 1.0 {
|
|
return Err(syn::Error::new(
|
|
n.span,
|
|
"alpha should be in the range `0 <= alpha < 1`",
|
|
));
|
|
}
|
|
if content.is_empty() {
|
|
Ok(Color::HSLA(hue, sat, light, alpha))
|
|
} else {
|
|
Err(content.error("unexpected trailing characters"))
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_color() {
|
|
for (input, output) in vec![
|
|
("#ffffffff", Color::HexRGBA(255, 255, 255, 255)),
|
|
("#ffffff", Color::HexRGB(255, 255, 255)),
|
|
("#fff", Color::HexRGB(255, 255, 255)),
|
|
("#\"fff\"", Color::HexRGB(255, 255, 255)),
|
|
("hsl(100, 50%, 50%)", Color::HSL(100.0, 50.0, 50.0)),
|
|
("hsla(60, 0%, 0%, 0.2)", Color::HSLA(60.0, 0.0, 0.0, 0.2)),
|
|
("black", Color::Black),
|
|
("yellow", Color::Yellow),
|
|
] {
|
|
match syn::parse_str::<Color>(input) {
|
|
Ok(c) => assert_eq!(c, output),
|
|
Err(e) => panic!("error parsing color {}: {}", input, e),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Util
|
|
// ====
|
|
|
|
impl<T> Parse for NonemptyCommaList<T>
|
|
where
|
|
T: Parse,
|
|
{
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let punctuated = Punctuated::<T, Token![,]>::parse_separated_nonempty(s)?;
|
|
let mut iter = punctuated.into_iter();
|
|
let first = iter.next().unwrap();
|
|
Ok(Self {
|
|
first,
|
|
rest: iter.collect(),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl<T> Parse for SingleOrDouble<T>
|
|
where
|
|
T: Parse,
|
|
{
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let first = T::parse(s)?;
|
|
let fork = s.fork();
|
|
Ok(match T::parse(&fork) {
|
|
Ok(second) => {
|
|
s.advance_to(&fork);
|
|
SingleOrDouble::Double {
|
|
vert: first,
|
|
horiz: second,
|
|
}
|
|
}
|
|
Err(_) => SingleOrDouble::Single(first),
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Either a float or an int, converted in either case to f64.
|
|
///
|
|
/// A trailing percent (`%`) character will be consumed if the number has no suffix. This is valid
|
|
/// according to the CSS tokeniser spec.
|
|
///
|
|
/// TODO This only works for floats for now. Although JS only supports floats, integer literals are
|
|
/// used in css.
|
|
#[derive(Debug)]
|
|
struct Number {
|
|
value: f64,
|
|
suffix: String,
|
|
span: Span,
|
|
}
|
|
|
|
impl Number {
|
|
fn empty_suffix(&self) -> syn::Result<()> {
|
|
if self.suffix != "" {
|
|
Err(syn::Error::new(
|
|
self.span,
|
|
"unexpected characters after number",
|
|
))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn check_value(&self, value: f64, suffix: &str) -> bool {
|
|
self.value == value && self.suffix == suffix
|
|
}
|
|
}
|
|
|
|
impl Parse for Number {
|
|
fn parse(s: ParseStream) -> syn::Result<Number> {
|
|
let lookahead = s.lookahead1();
|
|
let (value, mut span, mut suffix) = if lookahead.peek(syn::LitFloat) {
|
|
let tok = s.parse::<syn::LitFloat>()?;
|
|
let num = tok.base10_parse()?;
|
|
(num, tok.span(), tok.suffix().to_string())
|
|
} else if lookahead.peek(syn::LitInt) {
|
|
let tok = s.parse::<syn::LitInt>()?;
|
|
// u32 chosen because it can be safely converted into f64
|
|
let num = tok.base10_parse::<u32>()?;
|
|
(num.into(), tok.span(), tok.suffix().to_string())
|
|
} else {
|
|
return Err(lookahead.error());
|
|
};
|
|
if suffix.is_empty() {
|
|
// look for a `%` for the suffix
|
|
if s.peek(Token![%]) {
|
|
let tok = s.parse::<Token![%]>()?;
|
|
if let Some(extra_span) = span.join(tok.span) {
|
|
span = extra_span;
|
|
}
|
|
suffix.push('%');
|
|
// work-around using literal strings because the lexer can't support suffixes beginning
|
|
// with `e` for floats: https://github.com/rust-lang/rust/issues/67544
|
|
} else if s.peek(syn::LitStr) {
|
|
let tok = s.parse::<syn::LitStr>()?;
|
|
if let Some(extra_span) = span.join(tok.span()) {
|
|
span = extra_span;
|
|
}
|
|
suffix.push_str(&tok.value());
|
|
}
|
|
}
|
|
Ok(Number {
|
|
value,
|
|
suffix,
|
|
span,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_number() {
|
|
for (input, value, suffix) in vec![
|
|
("200", 200.0, ""),
|
|
("200.0", 200.0, ""),
|
|
("0", 0.0, ""),
|
|
("0in", 0.0, "in"),
|
|
] {
|
|
assert!(syn::parse_str::<Number>(input)
|
|
.unwrap()
|
|
.check_value(value, suffix),)
|
|
}
|
|
}
|
|
|
|
/// Something like `word-separated-hyphens`
|
|
#[derive(Debug)]
|
|
struct HyphenWord {
|
|
pub span: Span,
|
|
pub word: Option<String>,
|
|
/// List of tried matches - for building error.
|
|
tried: TryList,
|
|
}
|
|
|
|
impl HyphenWord {
|
|
pub fn new(span: Span, word: String) -> Self {
|
|
HyphenWord {
|
|
span,
|
|
word: Some(word),
|
|
tried: TryList::new(),
|
|
}
|
|
}
|
|
|
|
/// This allows HyphenWords to be empty. In this case the token cursor will not advance and the
|
|
/// returned word will be blank.
|
|
pub fn new_no_word(span: Span) -> Self {
|
|
HyphenWord {
|
|
span,
|
|
word: None,
|
|
tried: TryList::new(),
|
|
}
|
|
}
|
|
|
|
pub fn try_match(&self, other: &str) -> bool {
|
|
if Some(other) == self.word.as_ref().map(|s| s.as_str()) {
|
|
true
|
|
} else {
|
|
self.tried.add_literal(other);
|
|
false
|
|
}
|
|
}
|
|
|
|
pub fn add_expected(&self, ty: &str) {
|
|
self.tried.add(ty);
|
|
}
|
|
|
|
/// Panics if there were no calls to `try_match` before calling this function.
|
|
pub fn error(&self) -> syn::Error {
|
|
self.tried.to_error(self.span)
|
|
}
|
|
|
|
/// This is cheaper than peek-specific
|
|
pub fn peek(s: ParseStream) -> bool {
|
|
s.peek(Ident)
|
|
}
|
|
|
|
/// Peek the next HyphenWord without advancing the parser.
|
|
pub fn peek_specific(s: ParseStream) -> Option<String> {
|
|
let fork = s.fork();
|
|
match HyphenWord::parse(&fork) {
|
|
Ok(hw) => Some(hw.word.unwrap()),
|
|
Err(_) => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parse for HyphenWord {
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
let fork = s.fork();
|
|
let first = match fork.call(Ident::parse_any) {
|
|
Ok(v) => {
|
|
s.advance_to(&fork);
|
|
v
|
|
}
|
|
Err(_) => return Ok(HyphenWord::new_no_word(s.cursor().span())),
|
|
};
|
|
let mut word = first.to_string();
|
|
let mut span = first.span();
|
|
// This is potentially unbounded. Probably not be a problem but making a note anyway.
|
|
while s.peek(Token![-]) {
|
|
let hyphen = s.parse::<Token![-]>()?;
|
|
if let Some(joined) = span.join(hyphen.span) {
|
|
span = joined;
|
|
}
|
|
let part = s.call(Ident::parse_any)?;
|
|
write!(word, "-{}", part).unwrap();
|
|
if let Some(joined) = span.join(part.span()) {
|
|
span = joined;
|
|
}
|
|
}
|
|
Ok(HyphenWord::new(span, word))
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_hyphen_word() {
|
|
let word: HyphenWord = syn::parse_str("first-second-third").unwrap();
|
|
assert_eq!(word.word, Some("first-second-third".to_string()));
|
|
assert!(syn::parse_str::<HyphenWord>("first-second-").is_err());
|
|
assert!(syn::parse_str::<HyphenWord>("a a").is_err());
|
|
}
|
|
|
|
/// Keeps track of a list of tokens that have been tried.
|
|
#[derive(Debug)]
|
|
pub struct TryList(RefCell<BTreeSet<String>>);
|
|
|
|
impl TryList {
|
|
pub fn new() -> Self {
|
|
TryList(RefCell::new(BTreeSet::new()))
|
|
}
|
|
|
|
/// Same as add, but with quotes
|
|
pub fn add_literal(&self, lit: &str) {
|
|
self.add(format!("`{}`", lit));
|
|
}
|
|
|
|
pub fn add(&self, ty: impl Into<String>) {
|
|
self.0.borrow_mut().insert(ty.into());
|
|
}
|
|
|
|
fn to_error(&self, span: Span) -> syn::Error {
|
|
let tried = self.0.borrow();
|
|
let mut iter = tried.iter();
|
|
let start = iter.next().unwrap().to_owned();
|
|
let list = iter.fold(start, |mut acc, itm| {
|
|
write!(acc, ", {}", itm).unwrap();
|
|
acc
|
|
});
|
|
let error_msg = format!("expected one of {}", list);
|
|
syn::Error::new(span, error_msg)
|
|
}
|
|
}
|
|
|
|
/// Whether we are at the end of a rule. Either the stream will be empty, or there will be a
|
|
/// semi-colon.
|
|
fn finished_rule(s: ParseStream) -> bool {
|
|
s.is_empty() || s.peek(Token![;])
|
|
}
|
|
|
|
// Parsing integers
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
struct Integer<T> {
|
|
value: T,
|
|
}
|
|
|
|
impl<T> Integer<T> {
|
|
fn into_inner(self) -> T {
|
|
self.value
|
|
}
|
|
}
|
|
|
|
impl<T> Parse for Integer<T>
|
|
where
|
|
T: str::FromStr + fmt::Display + PartialOrd<T>,
|
|
<T as str::FromStr>::Err: fmt::Display,
|
|
{
|
|
fn parse(s: ParseStream) -> syn::Result<Self> {
|
|
Ok(Integer {
|
|
value: integer(s, ..)?,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Parse an integer, with an optional allowed range.
|
|
fn integer<T, R>(s: ParseStream, range: R) -> syn::Result<T>
|
|
where
|
|
R: RangeBounds<T> + fmt::Debug,
|
|
T: str::FromStr + fmt::Display + PartialOrd<T>,
|
|
<T as str::FromStr>::Err: fmt::Display,
|
|
{
|
|
let fixed = s.parse::<syn::LitInt>()?;
|
|
let span = fixed.span();
|
|
if fixed.suffix().is_empty() {
|
|
let fixed = fixed.base10_parse()?;
|
|
if range.contains(&fixed) {
|
|
Ok(fixed)
|
|
} else {
|
|
Err(syn::Error::new(
|
|
span,
|
|
format!(
|
|
"expected a number in the range {:?}, found {}",
|
|
range, fixed
|
|
),
|
|
))
|
|
}
|
|
} else {
|
|
Err(syn::Error::new(span, "the number should not have a suffix"))
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_integer() {
|
|
let x: Integer<u8> = syn::parse_str("123").unwrap();
|
|
assert_eq!(x.into_inner(), 123);
|
|
let x: syn::Result<Integer<u8>> = syn::parse_str("256");
|
|
assert!(x.is_err());
|
|
}
|
|
|
|
// tests
|
|
|
|
#[test]
|
|
fn downstream_bug1() {
|
|
let s: Styles = syn::parse_str(
|
|
"display: flex;
|
|
flex-direction: column;
|
|
flex-grow: 1;
|
|
flex-shrink: 0;",
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
s.rules,
|
|
vec![
|
|
Style::Display(Display::Flex),
|
|
Style::FlexDirection(FlexDirection::Column),
|
|
Style::FlexGrow(1.0),
|
|
Style::FlexShrink(0.0)
|
|
]
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn inline_logic() {
|
|
todo!()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// Use if you want to test that something parses, but not if it looks the same when
|
|
/// stringified. Example "border: 1px" -> "border:1px" but we still might want to check that
|
|
/// the former parses.
|
|
fn parse(input: &str) -> Style {
|
|
syn::parse_str(input).unwrap()
|
|
}
|
|
|
|
/// This function can be used to quickly write tests to check that a parse and a stringify are
|
|
/// opposites.
|
|
fn round_trip_style(input: &str) {
|
|
assert_eq!(&parse(input).to_string(), input);
|
|
}
|
|
|
|
#[test]
|
|
fn border_bottom_left_radius() {
|
|
round_trip_style("border-bottom-left-radius:30% 3px");
|
|
}
|
|
|
|
#[test]
|
|
fn border_bottom_right_radius() {
|
|
round_trip_style("border-bottom-right-radius:0 0");
|
|
}
|
|
|
|
#[test]
|
|
fn border_collapse() {
|
|
round_trip_style("border-collapse:collapse");
|
|
}
|
|
|
|
#[test]
|
|
fn border_width() {
|
|
round_trip_style("border-width:1px");
|
|
round_trip_style("border-width:0 2px 50pt 0");
|
|
}
|
|
}
|