diff --git a/src/color.rs b/src/color.rs index 11cc5b3b7..668105377 100644 --- a/src/color.rs +++ b/src/color.rs @@ -49,9 +49,14 @@ impl RgbaColor { impl FromStr for RgbaColor { type Err = ParseRgbaError; - /// Constructs a new color from a hex string like `7a03c2`. Do not specify a - /// leading `#`. + /// Constructs a new color from hex strings like the following: + /// - `#aef` (shorthand, with leading hashtag), + /// - `7a03c2` (without alpha), + /// - `abcdefff` (with alpha). + /// + /// Both lower and upper case is fine. fn from_str(hex_str: &str) -> Result { + let hex_str = hex_str.strip_prefix('#').unwrap_or(hex_str); if !hex_str.is_ascii() { return Err(ParseRgbaError); } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 9fe8e62e2..a3a387759 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -13,9 +13,6 @@ pub use resolve::*; pub use scanner::*; pub use tokens::*; -use std::str::FromStr; - -use crate::color::RgbaColor; use crate::diag::Pass; use crate::syntax::*; @@ -314,7 +311,7 @@ fn primary(p: &mut Parser) -> Option { Some(Token::Length(val, unit)) => Expr::Length(val, unit), Some(Token::Angle(val, unit)) => Expr::Angle(val, unit), Some(Token::Percent(p)) => Expr::Percent(p), - Some(Token::Hex(hex)) => Expr::Color(color(p, hex)), + Some(Token::Color(color)) => Expr::Color(color), Some(Token::Str(token)) => Expr::Str(string(p, token)), // No value. @@ -357,15 +354,6 @@ fn paren_call(p: &mut Parser, name: Spanned) -> Expr { }) } -/// Parse a color. -fn color(p: &mut Parser, hex: &str) -> RgbaColor { - RgbaColor::from_str(hex).unwrap_or_else(|_| { - // Replace color with black. - p.diag(error!(p.peek_span(), "invalid color")); - RgbaColor::new(0, 0, 0, 255) - }) -} - /// Parse a string. fn string(p: &mut Parser, token: TokenStr) -> String { if !token.terminated { diff --git a/src/parse/tokens.rs b/src/parse/tokens.rs index f5e5baaf9..056bbbbb1 100644 --- a/src/parse/tokens.rs +++ b/src/parse/tokens.rs @@ -1,6 +1,8 @@ use std::fmt::{self, Debug, Formatter}; +use std::str::FromStr; use super::{is_newline, Scanner}; +use crate::color::RgbaColor; use crate::geom::{AngularUnit, LengthUnit}; use crate::syntax::*; @@ -139,7 +141,7 @@ impl<'s> Iterator for Tokens<'s> { } // Hex values and strings. - '#' => self.hex(), + '#' => self.hex(start), '"' => self.string(), _ => Token::Invalid(self.s.eaten_from(start)), @@ -200,16 +202,11 @@ impl<'s> Tokens<'s> { if self.s.check(is_id_start) { self.s.eat(); self.s.eat_while(is_id_continue); - match self.s.eaten_from(start) { - "#let" => Token::Let, - "#if" => Token::If, - "#else" => Token::Else, - "#for" => Token::For, - "#while" => Token::While, - "#break" => Token::Break, - "#continue" => Token::Continue, - "#return" => Token::Return, - s => Token::Invalid(s), + let read = self.s.eaten_from(start); + if let Some(keyword) = keyword(read) { + keyword + } else { + Token::Invalid(read) } } else { Token::Hash @@ -310,15 +307,6 @@ impl<'s> Tokens<'s> { "not" => Token::Not, "and" => Token::And, "or" => Token::Or, - "let" => Token::Let, - "if" => Token::If, - "else" => Token::Else, - "for" => Token::For, - "in" => Token::In, - "while" => Token::While, - "break" => Token::Break, - "continue" => Token::Continue, - "return" => Token::Return, "none" => Token::None, "true" => Token::Bool(true), "false" => Token::Bool(false), @@ -379,9 +367,16 @@ impl<'s> Tokens<'s> { } } - fn hex(&mut self) -> Token<'s> { - // Allow more than `ascii_hexdigit` for better error recovery. - Token::Hex(self.s.eat_while(|c| c.is_ascii_alphanumeric())) + fn hex(&mut self, start: usize) -> Token<'s> { + self.s.eat_while(is_id_continue); + let read = self.s.eaten_from(start); + if let Some(keyword) = keyword(read) { + keyword + } else if let Ok(color) = RgbaColor::from_str(read) { + Token::Color(color) + } else { + Token::Invalid(read) + } } fn string(&mut self) -> Token<'s> { @@ -440,6 +435,21 @@ impl Debug for Tokens<'_> { } } +fn keyword(id: &str) -> Option> { + Some(match id { + "#let" => Token::Let, + "#if" => Token::If, + "#else" => Token::Else, + "#for" => Token::For, + "#in" => Token::In, + "#while" => Token::While, + "#break" => Token::Break, + "#continue" => Token::Continue, + "#return" => Token::Return, + _ => return None, + }) +} + #[cfg(test)] #[allow(non_snake_case)] mod tests { @@ -465,6 +475,10 @@ mod tests { Token::Str(TokenStr { string, terminated }) } + const fn Color(r: u8, g: u8, b: u8, a: u8) -> Token<'static> { + Token::Color(RgbaColor { r, g, b, a }) + } + /// Building blocks for suffix testing. /// /// We extend each test case with a collection of different suffixes to make @@ -495,7 +509,6 @@ mod tests { // Letter suffixes. ('a', Some(Markup), "hello", Text("hello")), ('a', Some(Markup), "πŸ’š", Text("πŸ’š")), - ('a', Some(Code), "if", If), ('a', Some(Code), "val", Ident("val")), ('a', Some(Code), "Ξ±", Ident("Ξ±")), ('a', Some(Code), "_", Ident("_")), @@ -510,10 +523,11 @@ mod tests { ('/', Some(Markup), "$ $", Math(" ", true, true)), ('/', Some(Markup), r"\\", Text(r"\")), ('/', Some(Markup), "#let", Let), + ('/', Some(Code), "#if", If), ('/', Some(Code), "(", LeftParen), ('/', Some(Code), ":", Colon), ('/', Some(Code), "+=", PlusEq), - ('/', Some(Code), "#123", Hex("123")), + ('/', Some(Code), "#123", Color(0x11, 0x22, 0x33, 0xff)), ]; macro_rules! t { @@ -633,6 +647,7 @@ mod tests { ("if", If), ("else", Else), ("for", For), + ("in", In), ("while", While), ("break", Break), ("continue", Continue), @@ -640,7 +655,7 @@ mod tests { ]; for &(s, t) in &both { - t!(Code[" "]: s => t); + t!(Code[" "]: format!("#{}", s) => t); t!(Markup[" "]: format!("#{}", s) => t); t!(Markup[" "]: format!("#{0}#{0}", s) => t, t); t!(Markup[" /"]: format!("# {}", s) => Hash, Space(0), Text(s)); @@ -650,7 +665,6 @@ mod tests { ("not", Not), ("and", And), ("or", Or), - ("in", In), ("none", Token::None), ("false", Bool(false)), ("true", Bool(true)), @@ -854,13 +868,10 @@ mod tests { } #[test] - fn test_tokenize_hex() { - // Test basic hex expressions. - t!(Code[" /"]: "#6ae6dd" => Hex("6ae6dd")); - t!(Code[" /"]: "#8A083c" => Hex("8A083c")); - - // Test with non-hex letters. - t!(Code[" /"]: "#PQ" => Hex("PQ")); + fn test_tokenize_color() { + t!(Code[" /"]: "#ABC" => Color(0xAA, 0xBB, 0xCC, 0xff)); + t!(Code[" /"]: "#6ae6dd" => Color(0x6a, 0xe6, 0xdd, 0xff)); + t!(Code[" /"]: "#8A083caf" => Color(0x8A, 0x08, 0x3c, 0xaf)); } #[test] @@ -924,11 +935,11 @@ mod tests { t!(Both: "/**/*/" => BlockComment(""), Token::Invalid("*/")); // Test invalid expressions. - t!(Code: r"\" => Invalid(r"\")); - t!(Code: "πŸŒ“" => Invalid("πŸŒ“")); - t!(Code: r"\:" => Invalid(r"\"), Colon); - t!(Code: "meal⌚" => Ident("meal"), Invalid("⌚")); - t!(Code[" /"]: r"\a" => Invalid(r"\"), Ident("a")); + t!(Code: r"\" => Invalid(r"\")); + t!(Code: "πŸŒ“" => Invalid("πŸŒ“")); + t!(Code: r"\:" => Invalid(r"\"), Colon); + t!(Code: "meal⌚" => Ident("meal"), Invalid("⌚")); + t!(Code[" /"]: r"\a" => Invalid(r"\"), Ident("a")); // Test invalid number suffixes. t!(Code[" /"]: "1foo" => Invalid("1foo")); @@ -936,7 +947,8 @@ mod tests { t!(Code: "1%%" => Percent(1.0), Invalid("%")); // Test invalid keyword. - t!(Markup[" /"]: "#-" => Hash, Text("-")); - t!(Markup[" "]: "#do" => Invalid("#do")) + t!(Markup[" /"]: "#-" => Hash, Text("-")); + t!(Markup[" /"]: "#do" => Invalid("#do")); + t!(Code[" /"]: r"#letter" => Invalid(r"#letter")); } } diff --git a/src/syntax/token.rs b/src/syntax/token.rs index 432b4dc5b..43797f75d 100644 --- a/src/syntax/token.rs +++ b/src/syntax/token.rs @@ -1,3 +1,4 @@ +use crate::color::RgbaColor; use crate::geom::{AngularUnit, LengthUnit}; /// A minimal semantic entity of source code. @@ -71,26 +72,26 @@ pub enum Token<'s> { And, /// The `or` operator. Or, - /// The `let` / `#let` keyword. - Let, - /// The `if` / `#if` keyword. - If, - /// The `else` / `#else` keyword. - Else, - /// The `for` / `#for` keyword. - For, - /// The `in` / `#in` keyword. - In, - /// The `while` / `#while` keyword. - While, - /// The `break` / `#break` keyword. - Break, - /// The `continue` / `#continue` keyword. - Continue, - /// The `return` / `#return` keyword. - Return, /// The none literal: `none`. None, + /// The `#let` keyword. + Let, + /// The `#if` keyword. + If, + /// The `#else` keyword. + Else, + /// The `#for` keyword. + For, + /// The `#in` keyword. + In, + /// The `#while` keyword. + While, + /// The `#break` keyword. + Break, + /// The `#continue` keyword. + Continue, + /// The `#return` keyword. + Return, /// One or more whitespace characters. /// /// The contained `usize` denotes the number of newlines that were contained @@ -124,8 +125,8 @@ pub enum Token<'s> { /// _Note_: `50%` is stored as `50.0` here, as in the corresponding /// [literal](super::Expr::Percent). Percent(f64), - /// A hex value: `#20d82a`. - Hex(&'s str), + /// A color value: `#20d82a`. + Color(RgbaColor), /// A quoted string: `"..."`. Str(TokenStr<'s>), /// Two slashes followed by inner contents, terminated with a newline: @@ -223,16 +224,16 @@ impl<'s> Token<'s> { Self::Not => "operator `not`", Self::And => "operator `and`", Self::Or => "operator `or`", - Self::Let => "keyword `let`", - Self::If => "keyword `if`", - Self::Else => "keyword `else`", - Self::For => "keyword `for`", - Self::In => "keyword `in`", - Self::While => "keyword `while`", - Self::Break => "keyword `break`", - Self::Continue => "keyword `continue`", - Self::Return => "keyword `return`", Self::None => "`none`", + Self::Let => "keyword `#let`", + Self::If => "keyword `#if`", + Self::Else => "keyword `#else`", + Self::For => "keyword `#for`", + Self::In => "keyword `#in`", + Self::While => "keyword `#while`", + Self::Break => "keyword `#break`", + Self::Continue => "keyword `#continue`", + Self::Return => "keyword `#return`", Self::Space(_) => "space", Self::Text(_) => "text", Self::Raw(_) => "raw block", @@ -245,7 +246,7 @@ impl<'s> Token<'s> { Self::Length(..) => "length", Self::Angle(..) => "angle", Self::Percent(_) => "percentage", - Self::Hex(_) => "hex value", + Self::Color(_) => "color", Self::Str(_) => "string", Self::LineComment(_) => "line comment", Self::BlockComment(_) => "block comment", diff --git a/tests/lang/ref/blocks.png b/tests/lang/ref/blocks.png index 22a5722ad..bc96be95a 100644 Binary files a/tests/lang/ref/blocks.png and b/tests/lang/ref/blocks.png differ diff --git a/tests/lang/ref/values.png b/tests/lang/ref/values.png index d4411cc80..df46bd2d9 100644 Binary files a/tests/lang/ref/values.png and b/tests/lang/ref/values.png differ diff --git a/tests/lang/typ/blocks.typ b/tests/lang/typ/blocks.typ index faef0987f..79c092a2b 100644 --- a/tests/lang/typ/blocks.typ +++ b/tests/lang/typ/blocks.typ @@ -19,8 +19,3 @@ // Missing closing bracket in template expression. // Error: 1:11-1:11 expected closing bracket {[_] + [4_} - -// Opening brace is ignored after one expression is parsed. -// Error: 2:4-2:5 unexpected hex value -// Error: 1:5-1:6 unexpected opening brace -{5 #{}*.* diff --git a/tests/lang/typ/bracket-call.typ b/tests/lang/typ/bracket-call.typ index 73250e08b..fb6ef5570 100644 --- a/tests/lang/typ/bracket-call.typ +++ b/tests/lang/typ/bracket-call.typ @@ -80,7 +80,7 @@ #let x = "string" [x] -// Error: 1:2-1:3 expected function name, found hex value +// Error: 1:2-1:3 expected function name, found invalid token [# 1] // Error: 4:1-4:1 expected function name diff --git a/tests/lang/typ/if.typ b/tests/lang/typ/if.typ index f7de7716b..68f345ca1 100644 --- a/tests/lang/typ/if.typ +++ b/tests/lang/typ/if.typ @@ -53,7 +53,7 @@ a#if true {} a#if true [b] #else c // Lone else. -// Error: 2:1-2:6 unexpected keyword `else` +// Error: 2:1-2:6 unexpected keyword `#else` // Error: 1:8-1:8 expected function name #else [] diff --git a/tests/lang/typ/values.typ b/tests/lang/typ/values.typ index cab63044e..55712c4db 100644 --- a/tests/lang/typ/values.typ +++ b/tests/lang/typ/values.typ @@ -32,9 +32,6 @@ // Colors. {#f7a20500} \ -// Error: 1:2-1:5 invalid color -{#a5} - // Strings and escaping. {"hi"} \ {"a\n[]\"\u{1F680}string"} \