From 28c3a797ec7a6fbe3f5069604e5b19a00ae52344 Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Tue, 14 Jul 2020 19:09:58 +0200 Subject: [PATCH 1/3] Add named tuples and hex color tokens --- src/syntax/expr.rs | 122 +++++++++++++++++++++++++++++++++++++ src/syntax/parsing.rs | 136 ++++++++++++++++++++++++++++++++++++++---- src/syntax/test.rs | 10 +++- src/syntax/tokens.rs | 25 +++++++- 4 files changed, 281 insertions(+), 12 deletions(-) diff --git a/src/syntax/expr.rs b/src/syntax/expr.rs index b5bce2cd1..b7256c849 100644 --- a/src/syntax/expr.rs +++ b/src/syntax/expr.rs @@ -2,6 +2,8 @@ use std::fmt::{self, Write, Debug, Formatter}; use std::iter::FromIterator; +use std::ops::Deref; +use std::u8; use crate::error::Errors; use crate::size::Size; @@ -23,8 +25,12 @@ pub enum Expr { Size(Size), /// A bool: `true, false`. Bool(bool), + /// A color value, including the alpha channel: `#f79143ff` + Color(RgbaColor), /// A tuple: `(false, 12cm, "hi")`. Tuple(Tuple), + /// A named tuple: `cmyk(37.7, 0, 3.9, 1.1)`. + NamedTuple(NamedTuple), /// An object: `{ fit: false, size: 12pt }`. Object(Object), } @@ -39,7 +45,9 @@ impl Expr { Number(_) => "number", Size(_) => "size", Bool(_) => "bool", + Color(_) => "color", Tuple(_) => "tuple", + NamedTuple(_) => "named tuple", Object(_) => "object", } } @@ -54,7 +62,9 @@ impl Debug for Expr { Number(n) => n.fmt(f), Size(s) => s.fmt(f), Bool(b) => b.fmt(f), + Color(c) => c.fmt(f), Tuple(t) => t.fmt(f), + NamedTuple(t) => t.fmt(f), Object(o) => o.fmt(f), } } @@ -97,6 +107,81 @@ impl Debug for Ident { } } +/// An 8-bit RGBA color. +/// +/// # Example +/// ```typst +/// [box: background=#423abaff] +/// ^^^^^^^^ +/// ``` +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct RgbaColor { + r: u8, + g: u8, + b: u8, + a: u8, +} + +impl RgbaColor { + pub fn new(r: u8, g: u8, b: u8, a: u8) -> RgbaColor { + RgbaColor { r, g, b, a } + } + + pub fn from_str(hex_str: &str) -> Option { + let len = hex_str.len(); + let permissable = &[3, 4, 6, 8]; + + if !permissable.contains(&len) { + return None; + } + + let long = len == 6 || len == 8; + let alpha = len == 4 || len == 8; + let mut values: [u8; 4] = [255; 4]; + + for elem in if alpha { 0..4 } else { 0..3 } { + let item_len = if long { 2 } else { 1 }; + let pos = elem * item_len; + + if let Ok(val) = u8::from_str_radix( + &hex_str[pos..(pos+item_len)], 16) { + values[elem] = val; + } else { + // Some non-hexadecimal characters slipped into the color + return None; + } + + if !long { + // Duplicate number for shorthand notation, i.e. `a` -> `aa` + values[elem] += values[elem] * 16; + } + } + + Some( + RgbaColor::new(values[0], values[1], values[2], values[3]) + ) + } +} + +impl Debug for RgbaColor { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if f.alternate() { + f.write_str("rgba(")?; + f.write_fmt(format_args!("r: {:02}, ", self.r))?; + f.write_fmt(format_args!("g: {:02}, ", self.g))?; + f.write_fmt(format_args!("b: {:02}, ", self.b))?; + f.write_fmt(format_args!("a: {:02}", self.a))?; + f.write_char(')') + } else { + f.write_char('#')?; + f.write_fmt(format_args!("{:02x}", self.r))?; + f.write_fmt(format_args!("{:02x}", self.g))?; + f.write_fmt(format_args!("{:02x}", self.b))?; + f.write_fmt(format_args!("{:02x}", self.a)) + } + } +} + /// An untyped sequence of expressions. /// /// # Example @@ -185,6 +270,43 @@ impl Debug for Tuple { } } +/// A named, untyped sequence of expressions. +/// +/// # Example +/// ```typst +/// hsl(93, 10, 19.4) +/// ``` +#[derive(Clone, PartialEq)] +pub struct NamedTuple { + pub name: Spanned, + /// The elements of the tuple. + pub tuple: Spanned, +} + +impl NamedTuple { + /// Create a named tuple from a tuple. + pub fn new(name: Spanned, tuple: Spanned) -> NamedTuple { + NamedTuple { name, tuple } + } +} + +impl Deref for NamedTuple { + type Target = Tuple; + + fn deref(&self) -> &Self::Target { + &self.tuple.v + } +} + +impl Debug for NamedTuple { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.debug_struct("named tuple") + .field("name", &self.name) + .field("values", &self.tuple) + .finish() + } +} + /// A key-value collection of identifiers and associated expressions. /// /// The pairs themselves are not spanned, but the combined spans can easily be diff --git a/src/syntax/parsing.rs b/src/syntax/parsing.rs index 2fb8b58ac..c85a9c31d 100644 --- a/src/syntax/parsing.rs +++ b/src/syntax/parsing.rs @@ -187,6 +187,14 @@ impl<'s> FuncParser<'s> { // If we have an identifier we might have a keyword argument, // otherwise its for sure a postional argument. if let Some(ident) = p.parse_ident() { + // This could still be a named tuple + if let Some(Token::LeftParen) = p.peekv() { + return Ok(FuncArg::Pos( + p.parse_named_tuple(ident) + .map(|t| Expr::NamedTuple(t)) + )); + } + p.skip_whitespace(); if let Some(Token::Equals) = p.peekv() { @@ -223,7 +231,16 @@ impl<'s> FuncParser<'s> { } Some(match first.v { - Token::ExprIdent(i) => take!((Expr::Ident(Ident(i.to_string())))), + Token::ExprIdent(i) => { + let name = take!(Ident(i.to_string())); + + // This could be a named tuple or an identifier + if let Some(Token::LeftParen) = self.peekv() { + self.parse_named_tuple(name).map(|t| Expr::NamedTuple(t)) + } else { + name.map(|i| Expr::Ident(i)) + } + }, Token::ExprStr { string, terminated } => { if !terminated { self.expected_at("quote", first.span.end); @@ -235,16 +252,28 @@ impl<'s> FuncParser<'s> { Token::ExprNumber(n) => take!(Expr::Number(n)), Token::ExprSize(s) => take!(Expr::Size(s)), Token::ExprBool(b) => take!(Expr::Bool(b)), + Token::ExprHex(s) => { + let color_opt = RgbaColor::from_str(s); - Token::LeftParen => self.parse_tuple(), - Token::LeftBrace => self.parse_object(), + if let Some(color) = color_opt { + take!(Expr::Color(color)) + } else { + // Heal color by assuming black + self.feedback.errors.push(err!(first.span; + "expected valid color, found invalid color #{}", s)); + take!(Expr::Color(RgbaColor::new(0, 0, 0, 255))) + } + }, + + Token::LeftParen => self.parse_tuple().map(|t| Expr::Tuple(t)), + Token::LeftBrace => self.parse_object().map(|o| Expr::Object(o)), _ => return None, }) } /// Parse a tuple expression: `(, ...)`. - fn parse_tuple(&mut self) -> Spanned { + fn parse_tuple(&mut self) -> Spanned { let token = self.eat(); debug_assert_eq!(token.map(Spanned::value), Some(Token::LeftParen)); @@ -252,11 +281,18 @@ impl<'s> FuncParser<'s> { // missing a `value` when an invalid token is encoutered. self.parse_collection(Some(Token::RightParen), |p| p.parse_expr().ok_or(("value", None))) - .map(|tuple| Expr::Tuple(tuple)) + } + + /// Parse a tuple expression: `name(, ...)` with a given identifier. + fn parse_named_tuple(&mut self, name: Spanned) -> Spanned { + let tuple = self.parse_tuple(); + let start = name.span.start.clone(); + let end = tuple.span.end.clone(); + Spanned::new(NamedTuple::new(name, tuple), Span::new(start, end)) } /// Parse an object expression: `{ : , ... }`. - fn parse_object(&mut self) -> Spanned { + fn parse_object(&mut self) -> Spanned { let token = self.eat(); debug_assert_eq!(token.map(Spanned::value), Some(Token::LeftBrace)); @@ -282,7 +318,7 @@ impl<'s> FuncParser<'s> { let value = p.parse_expr().ok_or(("value", None))?; Ok(Pair { key, value }) - }).map(|object| Expr::Object(object)) + }) } /// Parse a comma-separated collection where each item is parsed through @@ -513,6 +549,14 @@ mod tests { fn Id(text: &str) -> Expr { Expr::Ident(Ident(text.to_string())) } fn Str(text: &str) -> Expr { Expr::Str(text.to_string()) } fn Pt(points: f32) -> Expr { Expr::Size(Size::pt(points)) } + fn ClrS(color: &str) -> Expr { + Expr::Color( + RgbaColor::from_str(color).expect("Test color invalid") + ) + } + fn Clr(r: u8, g: u8, b: u8, a: u8) -> Expr { + Expr::Color(RgbaColor::new(r, g, b, a)) + } fn T(text: &str) -> Node { Node::Text(text.to_string()) } /// Create a raw text node. @@ -529,6 +573,16 @@ mod tests { }; } + /// Create a named tuple expression. + macro_rules! named_tuple { + ($name:expr $(, $items:expr)* $(,)?) => { + Expr::NamedTuple(NamedTuple::new( + zspan(Ident($name.to_string())), + zspan(Tuple { items: spanned![vec $($items),*].0 }) + )) + }; + } + /// Create an object expression. macro_rules! object { ($($key:expr => $value:expr),* $(,)?) => { @@ -603,6 +657,15 @@ mod tests { (@body) => (None); } + #[test] + fn parse_color_strings() { + assert_eq!(Clr(0xf6, 0x12, 0x43, 0xff), ClrS("f61243ff")); + assert_eq!(Clr(0xb3, 0xd8, 0xb3, 0xff), ClrS("b3d8b3")); + assert_eq!(Clr(0xfc, 0xd2, 0xa9, 0xad), ClrS("fCd2a9AD")); + assert_eq!(Clr(0x22, 0x33, 0x33, 0xff), ClrS("233")); + assert_eq!(Clr(0x11, 0x11, 0x11, 0xbb), ClrS("111b")); + } + #[test] fn unescape_strings() { fn test(string: &str, expected: &str) { @@ -747,43 +810,96 @@ mod tests { p!("[val: 3.14]" => [func!("val": (Num(3.14)), {})]); p!("[val: 4.5cm]" => [func!("val": (Sz(Size::cm(4.5))), {})]); p!("[val: 12e1pt]" => [func!("val": (Pt(12e1)), {})]); + p!("[val: #f7a20500]" => [func!("val": (ClrS("f7a20500")), {})]); // Unclosed string. p!("[val: \"hello]" => [func!("val": (Str("hello]")), {})], [ (0:13, 0:13, "expected quote"), (0:13, 0:13, "expected closing bracket"), ]); + + //Invalid colors + p!("[val: #12345]" => [func!("val": (ClrS("000f")), {})], [ + (0:6, 0:12, "expected valid color, found invalid color #12345"), + ]); + p!("[val: #a5]" => [func!("val": (ClrS("000f")), {})], [ + (0:6, 0:9, "expected valid color, found invalid color #a5"), + ]); + p!("[val: #14b2ah]" => [func!("val": (ClrS("000f")), {})], [ + (0:6, 0:13, "expected valid color, found invalid color #14b2a"), + ]); + p!("[val: #f075ff011]" => [func!("val": (ClrS("000f")), {})], [ + (0:6, 0:16, "expected valid color, found invalid color #f075ff011"), + ]); } #[test] fn parse_tuples() { // Empty tuple p!("[val: ()]" => [func!("val": (tuple!()), {})]); + p!("[val: empty()]" => [func!("val": (named_tuple!("empty")), {})]); // Invalid value p!("[val: (🌎)]" => [func!("val": (tuple!()), {})], [(0:7, 0:8, "expected value, found invalid token")], ); + p!("[val: sound(\x07)]" => + [func!("val": (named_tuple!("sound")), {})], + [(0:12, 0:13, "expected value, found invalid token")], + ); + + // Invalid tuple name + p!("[val: πŸ‘ (\"abc\", 13e-5)]" => + [func!("val": (tuple!(Str("abc"), Num(13.0e-5))), {})], + [(0:6, 0:7, "expected argument, found invalid token")], + ); // Unclosed tuple p!("[val: (hello]" => [func!("val": (tuple!(Id("hello"))), {})], [(0:12, 0:12, "expected closing paren")], ); + p!("[val: lang(δΈ­ζ–‡]" => + [func!("val": (named_tuple!("lang", Id("δΈ­ζ–‡"))), {})], + [(0:13, 0:13, "expected closing paren")], + ); // Valid values p!("[val: (1, 2)]" => [func!("val": (tuple!(Num(1.0), Num(2.0))), {})]); p!("[val: (\"s\",)]" => [func!("val": (tuple!(Str("s"))), {})]); - + p!("[val: cmyk(1, 46, 0, 0)]" => + [func!("val": (named_tuple!( + "cmyk", Num(1.0), Num(46.0), Num(0.0), Num(0.0) + )), {})] + ); + p!("[val: items(\"fire\", #f93a6d)]" => + [func!("val": (named_tuple!( + "items", Str("fire"), ClrS("f93a6d") + )), {})] + ); + // Nested tuples - p!("[val: (1, (2))]" => [func!("val": (tuple!(Num(1.0), tuple!(Num(2.0)))), {})]); - + p!("[val: (1, (2))]" => + [func!("val": (tuple!(Num(1.0), tuple!(Num(2.0)))), {})] + ); + p!("[val: css(1pt, rgb(90, 102, 254), \"solid\")]" => + [func!("val": (named_tuple!( + "css", Pt(1.0), named_tuple!( + "rgb", Num(90.0), Num(102.0), Num(254.0) + ), Str("solid") + )), {})] + ); + // Invalid commas p!("[val: (,)]" => [func!("val": (tuple!()), {})], [(0:7, 0:8, "expected value, found comma")], ); + p!("[val: nose(,)]" => + [func!("val": (named_tuple!("nose")), {})], + [(0:11, 0:12, "expected value, found comma")], + ); p!("[val: (true false)]" => [func!("val": (tuple!(Bool(true), Bool(false))), {})], [(0:11, 0:11, "expected comma")], diff --git a/src/syntax/test.rs b/src/syntax/test.rs index 22ce45e50..ca47c0c55 100644 --- a/src/syntax/test.rs +++ b/src/syntax/test.rs @@ -1,7 +1,7 @@ use std::fmt::Debug; use super::func::FuncHeader; -use super::expr::{Expr, Tuple, Object}; +use super::expr::{Expr, Tuple, NamedTuple, Object}; use super::span::{Span, Spanned}; use super::tokens::Token; use super::*; @@ -127,6 +127,7 @@ impl SpanlessEq for DebugFn { impl SpanlessEq for Expr { fn spanless_eq(&self, other: &Expr) -> bool { match (self, other) { + (Expr::NamedTuple(a), Expr::NamedTuple(b)) => a.spanless_eq(b), (Expr::Tuple(a), Expr::Tuple(b)) => a.spanless_eq(b), (Expr::Object(a), Expr::Object(b)) => a.spanless_eq(b), (a, b) => a == b, @@ -142,6 +143,13 @@ impl SpanlessEq for Tuple { } } +impl SpanlessEq for NamedTuple { + fn spanless_eq(&self, other: &NamedTuple) -> bool { + self.name.v == other.name.v + && self.tuple.v.spanless_eq(&other.tuple.v) + } +} + impl SpanlessEq for Object { fn spanless_eq(&self, other: &Object) -> bool { self.pairs.len() == other.pairs.len() diff --git a/src/syntax/tokens.rs b/src/syntax/tokens.rs index cc65d9937..d3e062a84 100644 --- a/src/syntax/tokens.rs +++ b/src/syntax/tokens.rs @@ -78,6 +78,8 @@ pub enum Token<'s> { ExprSize(Size), /// A boolean in a function header: `true | false`. ExprBool(bool), + /// A hex value in a function header: `#20d82a` + ExprHex(&'s str), /// A star in body-text. Star, @@ -122,6 +124,7 @@ impl<'s> Token<'s> { ExprNumber(_) => "number", ExprSize(_) => "size", ExprBool(_) => "bool", + ExprHex(_) => "hex value", Star => "star", Underscore => "underscore", Backslash => "backslash", @@ -224,6 +227,21 @@ impl<'s> Iterator for Tokens<'s> { // Expressions or just strings. c => { let body = self.mode == Body; + + // This may be a hex expression + let hex_expr = if c == '#' && !body { + let payload = self.read_string_until(|n| { + match n { + '0'..='9' | 'a'..='f' | 'A'..='F' => false, + _ => true, + } + }, false, 0, 0).0; + + Some(ExprHex(payload)) + } else { + None + }; + let text = self.read_string_until(|n| { match n { c if c.is_whitespace() => true, @@ -235,7 +253,9 @@ impl<'s> Iterator for Tokens<'s> { } }, false, -(c.len_utf8() as isize), 0).0; - if self.mode == Header { + if let Some(hex_expr) = hex_expr { + hex_expr + } else if self.mode == Header { self.parse_expr(text) } else { Text(text) @@ -499,6 +519,7 @@ mod tests { ExprNumber as Num, ExprSize as Sz, ExprBool as Bool, + ExprHex as Hex, Text as T, }; @@ -587,6 +608,8 @@ mod tests { t!(Body, "c=d, " => [T("c=d,"), S(0)]); t!(Header, "(){}:=," => [LP, RP, LB, RB, Colon, Equals, Comma]); t!(Header, "a:b" => [Id("a"), Colon, Id("b")]); + t!(Header, "#6ae6dd" => [Hex("6ae6dd")]); + t!(Header, "#8A083c" => [Hex("8A083c")]); t!(Header, "a: true, x=1" => [Id("a"), Colon, S(0), Bool(true), Comma, S(0), Id("x"), Equals, Num(1.0)]); t!(Header, "=3.14" => [Equals, Num(3.14)]); t!(Header, "12.3e5" => [Num(12.3e5)]); From 1683ca87f797a6262f0a4f75bde0e23eca31798c Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Wed, 15 Jul 2020 20:57:26 +0200 Subject: [PATCH 2/3] =?UTF-8?q?Healed=20field=20for=20RgbaColors,=20CodeRe?= =?UTF-8?q?v=20feedback=20=F0=9F=A4=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/syntax/expr.rs | 72 +++++++++++++++++++++++++------------------ src/syntax/parsing.rs | 36 +++++++++++++--------- src/syntax/tokens.rs | 32 +++++++++---------- 3 files changed, 78 insertions(+), 62 deletions(-) diff --git a/src/syntax/expr.rs b/src/syntax/expr.rs index b7256c849..52efbe531 100644 --- a/src/syntax/expr.rs +++ b/src/syntax/expr.rs @@ -116,42 +116,57 @@ impl Debug for Ident { /// ``` #[derive(Clone, Eq, PartialEq, Hash)] pub struct RgbaColor { - r: u8, - g: u8, - b: u8, - a: u8, + /// Red channel. + pub r: u8, + /// Green channel. + pub g: u8, + /// Blue channel. + pub b: u8, + /// Alpha channel. + pub a: u8, + /// Indicates whether this is a user-provided value or a + /// default value provided as a fail-over by the parser. + /// This color may be overwritten if this property is true. + pub healed: bool, } impl RgbaColor { + /// Constructs a new color. pub fn new(r: u8, g: u8, b: u8, a: u8) -> RgbaColor { - RgbaColor { r, g, b, a } + RgbaColor { r, g, b, a, healed: false } } + /// Constructs a new color with the healed property set to true. + pub fn new_healed(r: u8, g: u8, b: u8, a: u8) -> RgbaColor { + RgbaColor { r, g, b, a, healed: true } + } + + /// Constructs a new color from a hex string like `7a03c2`. + /// Do not specify a leading `#`. pub fn from_str(hex_str: &str) -> Option { - let len = hex_str.len(); - let permissable = &[3, 4, 6, 8]; - - if !permissable.contains(&len) { + if !hex_str.is_ascii() { return None; } - let long = len == 6 || len == 8; + let len = hex_str.len(); + let long = len == 6 || len == 8; + let short = len == 3 || len == 4; let alpha = len == 4 || len == 8; + + if !long && !short { + return None; + } + let mut values: [u8; 4] = [255; 4]; for elem in if alpha { 0..4 } else { 0..3 } { let item_len = if long { 2 } else { 1 }; let pos = elem * item_len; - if let Ok(val) = u8::from_str_radix( - &hex_str[pos..(pos+item_len)], 16) { - values[elem] = val; - } else { - // Some non-hexadecimal characters slipped into the color - return None; - } + let item = &hex_str[pos..(pos+item_len)]; + values[elem] = u8::from_str_radix(item, 16).ok()?; - if !long { + if short { // Duplicate number for shorthand notation, i.e. `a` -> `aa` values[elem] += values[elem] * 16; } @@ -171,13 +186,18 @@ impl Debug for RgbaColor { f.write_fmt(format_args!("g: {:02}, ", self.g))?; f.write_fmt(format_args!("b: {:02}, ", self.b))?; f.write_fmt(format_args!("a: {:02}", self.a))?; - f.write_char(')') + f.write_char(')')?; } else { f.write_char('#')?; f.write_fmt(format_args!("{:02x}", self.r))?; f.write_fmt(format_args!("{:02x}", self.g))?; f.write_fmt(format_args!("{:02x}", self.b))?; - f.write_fmt(format_args!("{:02x}", self.a)) + f.write_fmt(format_args!("{:02x}", self.a))?; + } + if self.healed { + f.write_fmt(format_args!(" [healed]")) + } else { + Ok(()) } } } @@ -276,8 +296,9 @@ impl Debug for Tuple { /// ```typst /// hsl(93, 10, 19.4) /// ``` -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, Debug)] pub struct NamedTuple { + /// The name of the tuple and where it is in the user source. pub name: Spanned, /// The elements of the tuple. pub tuple: Spanned, @@ -298,15 +319,6 @@ impl Deref for NamedTuple { } } -impl Debug for NamedTuple { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.debug_struct("named tuple") - .field("name", &self.name) - .field("values", &self.tuple) - .finish() - } -} - /// A key-value collection of identifiers and associated expressions. /// /// The pairs themselves are not spanned, but the combined spans can easily be diff --git a/src/syntax/parsing.rs b/src/syntax/parsing.rs index c85a9c31d..772f2dc0e 100644 --- a/src/syntax/parsing.rs +++ b/src/syntax/parsing.rs @@ -253,15 +253,13 @@ impl<'s> FuncParser<'s> { Token::ExprSize(s) => take!(Expr::Size(s)), Token::ExprBool(b) => take!(Expr::Bool(b)), Token::ExprHex(s) => { - let color_opt = RgbaColor::from_str(s); - - if let Some(color) = color_opt { + if let Some(color) = RgbaColor::from_str(s) { take!(Expr::Color(color)) } else { // Heal color by assuming black self.feedback.errors.push(err!(first.span; - "expected valid color, found invalid color #{}", s)); - take!(Expr::Color(RgbaColor::new(0, 0, 0, 255))) + "invalid color")); + take!(Expr::Color(RgbaColor::new_healed(0, 0, 0, 255))) } }, @@ -286,8 +284,8 @@ impl<'s> FuncParser<'s> { /// Parse a tuple expression: `name(, ...)` with a given identifier. fn parse_named_tuple(&mut self, name: Spanned) -> Spanned { let tuple = self.parse_tuple(); - let start = name.span.start.clone(); - let end = tuple.span.end.clone(); + let start = name.span.start; + let end = tuple.span.end; Spanned::new(NamedTuple::new(name, tuple), Span::new(start, end)) } @@ -549,14 +547,22 @@ mod tests { fn Id(text: &str) -> Expr { Expr::Ident(Ident(text.to_string())) } fn Str(text: &str) -> Expr { Expr::Str(text.to_string()) } fn Pt(points: f32) -> Expr { Expr::Size(Size::pt(points)) } + fn ClrS(color: &str) -> Expr { Expr::Color( RgbaColor::from_str(color).expect("Test color invalid") ) } + fn ClrS_Healed() -> Expr { + let mut c = RgbaColor::from_str("000f") + .expect("Test color invalid"); + c.healed = true; + Expr::Color(c) + } fn Clr(r: u8, g: u8, b: u8, a: u8) -> Expr { Expr::Color(RgbaColor::new(r, g, b, a)) } + fn T(text: &str) -> Node { Node::Text(text.to_string()) } /// Create a raw text node. @@ -819,17 +825,17 @@ mod tests { ]); //Invalid colors - p!("[val: #12345]" => [func!("val": (ClrS("000f")), {})], [ - (0:6, 0:12, "expected valid color, found invalid color #12345"), + p!("[val: #12345]" => [func!("val": (ClrS_Healed()), {})], [ + (0:6, 0:12, "invalid color"), ]); - p!("[val: #a5]" => [func!("val": (ClrS("000f")), {})], [ - (0:6, 0:9, "expected valid color, found invalid color #a5"), + p!("[val: #a5]" => [func!("val": (ClrS_Healed()), {})], [ + (0:6, 0:9, "invalid color"), ]); - p!("[val: #14b2ah]" => [func!("val": (ClrS("000f")), {})], [ - (0:6, 0:13, "expected valid color, found invalid color #14b2a"), + p!("[val: #14b2ah]" => [func!("val": (ClrS_Healed()), {})], [ + (0:6, 0:13, "invalid color"), ]); - p!("[val: #f075ff011]" => [func!("val": (ClrS("000f")), {})], [ - (0:6, 0:16, "expected valid color, found invalid color #f075ff011"), + p!("[val: #f075ff011]" => [func!("val": (ClrS_Healed()), {})], [ + (0:6, 0:16, "invalid color"), ]); } diff --git a/src/syntax/tokens.rs b/src/syntax/tokens.rs index d3e062a84..07a1ac92b 100644 --- a/src/syntax/tokens.rs +++ b/src/syntax/tokens.rs @@ -224,24 +224,13 @@ impl<'s> Iterator for Tokens<'s> { // An escaped thing. '\\' if self.mode == Body => self.parse_escaped(), + // A hex expression. + '#' if self.mode == Header => self.parse_hex_value(), + // Expressions or just strings. c => { let body = self.mode == Body; - // This may be a hex expression - let hex_expr = if c == '#' && !body { - let payload = self.read_string_until(|n| { - match n { - '0'..='9' | 'a'..='f' | 'A'..='F' => false, - _ => true, - } - }, false, 0, 0).0; - - Some(ExprHex(payload)) - } else { - None - }; - let text = self.read_string_until(|n| { match n { c if c.is_whitespace() => true, @@ -253,9 +242,7 @@ impl<'s> Iterator for Tokens<'s> { } }, false, -(c.len_utf8() as isize), 0).0; - if let Some(hex_expr) = hex_expr { - hex_expr - } else if self.mode == Header { + if self.mode == Header { self.parse_expr(text) } else { Text(text) @@ -399,6 +386,17 @@ impl<'s> Tokens<'s> { } } + fn parse_hex_value(&mut self) -> Token<'s> { + // This will parse more than the permissable 0-9, a-f, A-F character + // ranges to provide nicer error messages later. + let payload = self.read_string_until( + |n| !n.is_ascii_alphanumeric(), + false, 0, 0 + ).0; + + ExprHex(payload) + } + fn parse_expr(&mut self, text: &'s str) -> Token<'s> { if let Ok(b) = text.parse::() { ExprBool(b) From e96f3830f19ed63ae8b43f8ce33f824237b34d82 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 15 Jul 2020 23:49:10 +0200 Subject: [PATCH 3/3] =?UTF-8?q?Use=20FromStr=20trait=20and=20formatting=20?= =?UTF-8?q?=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/syntax/expr.rs | 56 ++++++++++++++++++++++++-------------- src/syntax/parsing.rs | 62 ++++++++++++++++++++----------------------- src/syntax/tokens.rs | 10 +++---- 3 files changed, 69 insertions(+), 59 deletions(-) diff --git a/src/syntax/expr.rs b/src/syntax/expr.rs index 52efbe531..3f5357290 100644 --- a/src/syntax/expr.rs +++ b/src/syntax/expr.rs @@ -3,6 +3,7 @@ use std::fmt::{self, Write, Debug, Formatter}; use std::iter::FromIterator; use std::ops::Deref; +use std::str::FromStr; use std::u8; use crate::error::Errors; @@ -141,11 +142,16 @@ impl RgbaColor { RgbaColor { r, g, b, a, healed: true } } +} + +impl FromStr for RgbaColor { + type Err = ParseColorError; + /// Constructs a new color from a hex string like `7a03c2`. /// Do not specify a leading `#`. - pub fn from_str(hex_str: &str) -> Option { + fn from_str(hex_str: &str) -> Result { if !hex_str.is_ascii() { - return None; + return Err(ParseColorError); } let len = hex_str.len(); @@ -154,7 +160,7 @@ impl RgbaColor { let alpha = len == 4 || len == 8; if !long && !short { - return None; + return Err(ParseColorError); } let mut values: [u8; 4] = [255; 4]; @@ -164,17 +170,16 @@ impl RgbaColor { let pos = elem * item_len; let item = &hex_str[pos..(pos+item_len)]; - values[elem] = u8::from_str_radix(item, 16).ok()?; - + values[elem] = u8::from_str_radix(item, 16) + .map_err(|_| ParseColorError)?; + if short { // Duplicate number for shorthand notation, i.e. `a` -> `aa` values[elem] += values[elem] * 16; } } - Some( - RgbaColor::new(values[0], values[1], values[2], values[3]) - ) + Ok(RgbaColor::new(values[0], values[1], values[2], values[3])) } } @@ -182,23 +187,34 @@ impl Debug for RgbaColor { fn fmt(&self, f: &mut Formatter) -> fmt::Result { if f.alternate() { f.write_str("rgba(")?; - f.write_fmt(format_args!("r: {:02}, ", self.r))?; - f.write_fmt(format_args!("g: {:02}, ", self.g))?; - f.write_fmt(format_args!("b: {:02}, ", self.b))?; - f.write_fmt(format_args!("a: {:02}", self.a))?; + write!(f, "r: {:02}, ", self.r)?; + write!(f, "g: {:02}, ", self.g)?; + write!(f, "b: {:02}, ", self.b)?; + write!(f, "a: {:02}", self.a)?; f.write_char(')')?; } else { f.write_char('#')?; - f.write_fmt(format_args!("{:02x}", self.r))?; - f.write_fmt(format_args!("{:02x}", self.g))?; - f.write_fmt(format_args!("{:02x}", self.b))?; - f.write_fmt(format_args!("{:02x}", self.a))?; + write!(f, "{:02x}", self.r)?; + write!(f, "{:02x}", self.g)?; + write!(f, "{:02x}", self.b)?; + write!(f, "{:02x}", self.a)?; } if self.healed { - f.write_fmt(format_args!(" [healed]")) - } else { - Ok(()) + f.write_fmt(format_args!(" [healed]"))?; } + Ok(()) + } +} + +/// The error returned when parsing a [`RgbaColor`] from a string fails. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct ParseColorError; + +impl std::error::Error for ParseColorError {} + +impl fmt::Display for ParseColorError { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("invalid color") } } @@ -296,7 +312,7 @@ impl Debug for Tuple { /// ```typst /// hsl(93, 10, 19.4) /// ``` -#[derive(Clone, PartialEq, Debug)] +#[derive(Debug, Clone, PartialEq)] pub struct NamedTuple { /// The name of the tuple and where it is in the user source. pub name: Spanned, diff --git a/src/syntax/parsing.rs b/src/syntax/parsing.rs index 772f2dc0e..cfbf2bf3f 100644 --- a/src/syntax/parsing.rs +++ b/src/syntax/parsing.rs @@ -1,6 +1,7 @@ //! Parsing of source code into syntax models. use std::iter::FromIterator; +use std::str::FromStr; use crate::{Pass, Feedback}; use super::func::{FuncHeader, FuncArgs, FuncArg}; @@ -253,12 +254,11 @@ impl<'s> FuncParser<'s> { Token::ExprSize(s) => take!(Expr::Size(s)), Token::ExprBool(b) => take!(Expr::Bool(b)), Token::ExprHex(s) => { - if let Some(color) = RgbaColor::from_str(s) { + if let Ok(color) = RgbaColor::from_str(s) { take!(Expr::Color(color)) } else { // Heal color by assuming black - self.feedback.errors.push(err!(first.span; - "invalid color")); + self.feedback.errors.push(err!(first.span; "invalid color")); take!(Expr::Color(RgbaColor::new_healed(0, 0, 0, 255))) } }, @@ -284,9 +284,8 @@ impl<'s> FuncParser<'s> { /// Parse a tuple expression: `name(, ...)` with a given identifier. fn parse_named_tuple(&mut self, name: Spanned) -> Spanned { let tuple = self.parse_tuple(); - let start = name.span.start; - let end = tuple.span.end; - Spanned::new(NamedTuple::new(name, tuple), Span::new(start, end)) + let span = Span::merge(name.span, tuple.span); + Spanned::new(NamedTuple::new(name, tuple), span) } /// Parse an object expression: `{ : , ... }`. @@ -548,20 +547,17 @@ mod tests { fn Str(text: &str) -> Expr { Expr::Str(text.to_string()) } fn Pt(points: f32) -> Expr { Expr::Size(Size::pt(points)) } - fn ClrS(color: &str) -> Expr { - Expr::Color( - RgbaColor::from_str(color).expect("Test color invalid") - ) - } - fn ClrS_Healed() -> Expr { - let mut c = RgbaColor::from_str("000f") - .expect("Test color invalid"); - c.healed = true; - Expr::Color(c) - } fn Clr(r: u8, g: u8, b: u8, a: u8) -> Expr { Expr::Color(RgbaColor::new(r, g, b, a)) } + fn ClrStr(color: &str) -> Expr { + Expr::Color(RgbaColor::from_str(color).expect("invalid test color")) + } + fn ClrStrHealed() -> Expr { + let mut c = RgbaColor::from_str("000f").expect("invalid test color"); + c.healed = true; + Expr::Color(c) + } fn T(text: &str) -> Node { Node::Text(text.to_string()) } @@ -665,11 +661,11 @@ mod tests { #[test] fn parse_color_strings() { - assert_eq!(Clr(0xf6, 0x12, 0x43, 0xff), ClrS("f61243ff")); - assert_eq!(Clr(0xb3, 0xd8, 0xb3, 0xff), ClrS("b3d8b3")); - assert_eq!(Clr(0xfc, 0xd2, 0xa9, 0xad), ClrS("fCd2a9AD")); - assert_eq!(Clr(0x22, 0x33, 0x33, 0xff), ClrS("233")); - assert_eq!(Clr(0x11, 0x11, 0x11, 0xbb), ClrS("111b")); + assert_eq!(Clr(0xf6, 0x12, 0x43, 0xff), ClrStr("f61243ff")); + assert_eq!(Clr(0xb3, 0xd8, 0xb3, 0xff), ClrStr("b3d8b3")); + assert_eq!(Clr(0xfc, 0xd2, 0xa9, 0xad), ClrStr("fCd2a9AD")); + assert_eq!(Clr(0x22, 0x33, 0x33, 0xff), ClrStr("233")); + assert_eq!(Clr(0x11, 0x11, 0x11, 0xbb), ClrStr("111b")); } #[test] @@ -816,7 +812,7 @@ mod tests { p!("[val: 3.14]" => [func!("val": (Num(3.14)), {})]); p!("[val: 4.5cm]" => [func!("val": (Sz(Size::cm(4.5))), {})]); p!("[val: 12e1pt]" => [func!("val": (Pt(12e1)), {})]); - p!("[val: #f7a20500]" => [func!("val": (ClrS("f7a20500")), {})]); + p!("[val: #f7a20500]" => [func!("val": (ClrStr("f7a20500")), {})]); // Unclosed string. p!("[val: \"hello]" => [func!("val": (Str("hello]")), {})], [ @@ -825,16 +821,16 @@ mod tests { ]); //Invalid colors - p!("[val: #12345]" => [func!("val": (ClrS_Healed()), {})], [ + p!("[val: #12345]" => [func!("val": (ClrStrHealed()), {})], [ (0:6, 0:12, "invalid color"), ]); - p!("[val: #a5]" => [func!("val": (ClrS_Healed()), {})], [ + p!("[val: #a5]" => [func!("val": (ClrStrHealed()), {})], [ (0:6, 0:9, "invalid color"), ]); - p!("[val: #14b2ah]" => [func!("val": (ClrS_Healed()), {})], [ + p!("[val: #14b2ah]" => [func!("val": (ClrStrHealed()), {})], [ (0:6, 0:13, "invalid color"), ]); - p!("[val: #f075ff011]" => [func!("val": (ClrS_Healed()), {})], [ + p!("[val: #f075ff011]" => [func!("val": (ClrStrHealed()), {})], [ (0:6, 0:16, "invalid color"), ]); } @@ -874,19 +870,19 @@ mod tests { // Valid values p!("[val: (1, 2)]" => [func!("val": (tuple!(Num(1.0), Num(2.0))), {})]); p!("[val: (\"s\",)]" => [func!("val": (tuple!(Str("s"))), {})]); - p!("[val: cmyk(1, 46, 0, 0)]" => + p!("[val: cmyk(1, 46, 0, 0)]" => [func!("val": (named_tuple!( "cmyk", Num(1.0), Num(46.0), Num(0.0), Num(0.0) )), {})] ); - p!("[val: items(\"fire\", #f93a6d)]" => + p!("[val: items(\"fire\", #f93a6d)]" => [func!("val": (named_tuple!( - "items", Str("fire"), ClrS("f93a6d") + "items", Str("fire"), ClrStr("f93a6d") )), {})] ); - + // Nested tuples - p!("[val: (1, (2))]" => + p!("[val: (1, (2))]" => [func!("val": (tuple!(Num(1.0), tuple!(Num(2.0)))), {})] ); p!("[val: css(1pt, rgb(90, 102, 254), \"solid\")]" => @@ -896,7 +892,7 @@ mod tests { ), Str("solid") )), {})] ); - + // Invalid commas p!("[val: (,)]" => [func!("val": (tuple!()), {})], diff --git a/src/syntax/tokens.rs b/src/syntax/tokens.rs index 07a1ac92b..adf29de23 100644 --- a/src/syntax/tokens.rs +++ b/src/syntax/tokens.rs @@ -230,7 +230,7 @@ impl<'s> Iterator for Tokens<'s> { // Expressions or just strings. c => { let body = self.mode == Body; - + let text = self.read_string_until(|n| { match n { c if c.is_whitespace() => true, @@ -389,12 +389,10 @@ impl<'s> Tokens<'s> { fn parse_hex_value(&mut self) -> Token<'s> { // This will parse more than the permissable 0-9, a-f, A-F character // ranges to provide nicer error messages later. - let payload = self.read_string_until( - |n| !n.is_ascii_alphanumeric(), + ExprHex(self.read_string_until( + |n| !n.is_ascii_alphanumeric(), false, 0, 0 - ).0; - - ExprHex(payload) + ).0) } fn parse_expr(&mut self, text: &'s str) -> Token<'s> {