Add named tuples and hex color tokens

This commit is contained in:
Martin Haug 2020-07-14 19:09:58 +02:00
parent 1658b00282
commit 28c3a797ec
4 changed files with 281 additions and 12 deletions

View File

@ -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<RgbaColor> {
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<Ident>,
/// The elements of the tuple.
pub tuple: Spanned<Tuple>,
}
impl NamedTuple {
/// Create a named tuple from a tuple.
pub fn new(name: Spanned<Ident>, tuple: Spanned<Tuple>) -> 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

View File

@ -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: `(<expr>, ...)`.
fn parse_tuple(&mut self) -> Spanned<Expr> {
fn parse_tuple(&mut self) -> Spanned<Tuple> {
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(<expr>, ...)` with a given identifier.
fn parse_named_tuple(&mut self, name: Spanned<Ident>) -> Spanned<NamedTuple> {
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: `{ <key>: <value>, ... }`.
fn parse_object(&mut self) -> Spanned<Expr> {
fn parse_object(&mut self) -> Spanned<Object> {
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")],

View File

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

View File

@ -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)]);