|
@@ -0,0 +1,2411 @@
|
|
|
+//! 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");
|
|
|
+ }
|
|
|
+}
|