diff --git a/src/syntax/expr.rs b/src/syntax/expr.rs index b5bce2cd1..3f5357290 100644 --- a/src/syntax/expr.rs +++ b/src/syntax/expr.rs @@ -2,6 +2,9 @@ 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; use crate::size::Size; @@ -23,8 +26,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 +46,9 @@ impl Expr { Number(_) => "number", Size(_) => "size", Bool(_) => "bool", + Color(_) => "color", Tuple(_) => "tuple", + NamedTuple(_) => "named tuple", Object(_) => "object", } } @@ -54,7 +63,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 +108,116 @@ impl Debug for Ident { } } +/// An 8-bit RGBA color. +/// +/// # Example +/// ```typst +/// [box: background=#423abaff] +/// ^^^^^^^^ +/// ``` +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct RgbaColor { + /// 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, 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 } + } + +} + +impl FromStr for RgbaColor { + type Err = ParseColorError; + + /// Constructs a new color from a hex string like `7a03c2`. + /// Do not specify a leading `#`. + fn from_str(hex_str: &str) -> Result { + if !hex_str.is_ascii() { + return Err(ParseColorError); + } + + 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 Err(ParseColorError); + } + + 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; + + let item = &hex_str[pos..(pos+item_len)]; + 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; + } + } + + Ok(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(")?; + 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('#')?; + 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]"))?; + } + 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") + } +} + /// An untyped sequence of expressions. /// /// # Example @@ -185,6 +306,35 @@ impl Debug for Tuple { } } +/// A named, untyped sequence of expressions. +/// +/// # Example +/// ```typst +/// hsl(93, 10, 19.4) +/// ``` +#[derive(Debug, Clone, PartialEq)] +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, +} + +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 + } +} + /// 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..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}; @@ -187,6 +188,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 +232,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 +253,25 @@ 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) => { + 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")); + take!(Expr::Color(RgbaColor::new_healed(0, 0, 0, 255))) + } + }, - Token::LeftParen => self.parse_tuple(), - Token::LeftBrace => self.parse_object(), + 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 +279,17 @@ 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 span = Span::merge(name.span, tuple.span); + Spanned::new(NamedTuple::new(name, tuple), span) } /// 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 +315,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 +546,19 @@ 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 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()) } /// Create a raw text node. @@ -529,6 +575,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 +659,15 @@ mod tests { (@body) => (None); } + #[test] + fn parse_color_strings() { + 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] fn unescape_strings() { fn test(string: &str, expected: &str) { @@ -747,43 +812,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": (ClrStr("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": (ClrStrHealed()), {})], [ + (0:6, 0:12, "invalid color"), + ]); + p!("[val: #a5]" => [func!("val": (ClrStrHealed()), {})], [ + (0:6, 0:9, "invalid color"), + ]); + p!("[val: #14b2ah]" => [func!("val": (ClrStrHealed()), {})], [ + (0:6, 0:13, "invalid color"), + ]); + p!("[val: #f075ff011]" => [func!("val": (ClrStrHealed()), {})], [ + (0:6, 0:16, "invalid color"), + ]); } #[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"), ClrStr("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..adf29de23 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", @@ -221,9 +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; + let text = self.read_string_until(|n| { match n { c if c.is_whitespace() => true, @@ -379,6 +386,15 @@ 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. + ExprHex(self.read_string_until( + |n| !n.is_ascii_alphanumeric(), + false, 0, 0 + ).0) + } + fn parse_expr(&mut self, text: &'s str) -> Token<'s> { if let Ok(b) = text.parse::() { ExprBool(b) @@ -499,6 +515,7 @@ mod tests { ExprNumber as Num, ExprSize as Sz, ExprBool as Bool, + ExprHex as Hex, Text as T, }; @@ -587,6 +604,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)]);