Require hashtag for all keywords 💂‍♀️

This commit is contained in:
Laurenz 2021-01-26 21:11:44 +01:00
parent 7ced99bcd7
commit aaa48403cd
10 changed files with 94 additions and 96 deletions

View File

@ -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<Self, Self::Err> {
let hex_str = hex_str.strip_prefix('#').unwrap_or(hex_str);
if !hex_str.is_ascii() {
return Err(ParseRgbaError);
}

View File

@ -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<Expr> {
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<Ident>) -> 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 {

View File

@ -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<Token<'static>> {
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"));
}
}

View File

@ -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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

After

Width:  |  Height:  |  Size: 640 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@ -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 #{}*.*

View File

@ -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

View File

@ -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 []

View File

@ -32,9 +32,6 @@
// Colors.
{#f7a20500} \
// Error: 1:2-1:5 invalid color
{#a5}
// Strings and escaping.
{"hi"} \
{"a\n[]\"\u{1F680}string"} \