From aaa48403cdd3d8499584eeca4103865d6425ac1b Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 26 Jan 2021 21:11:44 +0100 Subject: [PATCH] =?UTF-8?q?Require=20hashtag=20for=20all=20keywords=20?= =?UTF-8?q?=F0=9F=92=82=E2=80=8D=E2=99=80=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/color.rs | 9 ++- src/parse/mod.rs | 14 +---- src/parse/tokens.rs | 94 ++++++++++++++++++-------------- src/syntax/token.rs | 61 +++++++++++---------- tests/lang/ref/blocks.png | Bin 799 -> 640 bytes tests/lang/ref/values.png | Bin 8651 -> 8143 bytes tests/lang/typ/blocks.typ | 5 -- tests/lang/typ/bracket-call.typ | 2 +- tests/lang/typ/if.typ | 2 +- tests/lang/typ/values.typ | 3 - 10 files changed, 94 insertions(+), 96 deletions(-) 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 22a5722ad42df38500fed738a3e17135388d9d1b..bc96be95a11b9f54f20ca200f6416dba69537a86 100644 GIT binary patch literal 640 zcmeAS@N?(olHy`uVBq!ia0y~yU}OQZk8uDA20@dBcNrL%+&x_!Ln>~)z2i8s*+HQ7 zVHpG80Y(c3_9KiI9W6c0YnWAB4jo|>ay#VlLifb8OirWiA?&{lX28&11hwkzc*MpSe|rzd*QXD+P9%BS9eB*t`FMncEa5@dC_K< z!q*Xexv6iz)+jpds^2>Gz1!=JipjN`f%Z;#WOd?^%)^aRG5LmHJkq)quRf7d+iwdv3ap` z^OmEwUj_EQKa*LaC#G~`PIoJZ+4H2Adz(+T9{7wi6hIOW>jgn^?{wmQ4YPZ7qm_dD RT5V9$@^tlcS?83{1OQHn?mqwk literal 799 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 JcL@9N{{n;dkP-j@ delta 8311 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