diff options
| author | Laurenz <laurmaedje@gmail.com> | 2020-08-16 22:14:27 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2020-08-16 22:39:21 +0200 |
| commit | 30f16bbf6431ca0c174ca0a1abaa6a13ef50ab06 (patch) | |
| tree | f5a5c0adad15840ebe24b39e77ff467862067c91 /src/compute | |
| parent | 9f6137d8a829fe8f34554623495fa620252a0184 (diff) | |
Add Value type and replace dyn-nodes with call-exprs 🏗
- In addition to syntax trees there are now `Value`s, which syntax trees can be evaluated into (e.g. the tree is `5+5` and the value is `10`)
- Parsing is completely pure, function calls are not parsed into nodes, but into simple call expressions, which are resolved later
- Functions aren't dynamic nodes anymore, but simply functions which receive their arguments as a table and the layouting context
- Functions may return any `Value`
- Layouting is powered by functions which return the new `Commands` value, which informs the layouting engine what to do
- When a function returns a non-`Commands` value, the layouter simply dumps the value into the document in monospace
Diffstat (limited to 'src/compute')
| -rw-r--r-- | src/compute/mod.rs | 5 | ||||
| -rw-r--r-- | src/compute/scope.rs | 50 | ||||
| -rw-r--r-- | src/compute/table.rs | 379 | ||||
| -rw-r--r-- | src/compute/value.rs | 474 |
4 files changed, 908 insertions, 0 deletions
diff --git a/src/compute/mod.rs b/src/compute/mod.rs new file mode 100644 index 00000000..ac278243 --- /dev/null +++ b/src/compute/mod.rs @@ -0,0 +1,5 @@ +//! Building blocks for the computational part. + +pub mod scope; +pub mod table; +pub mod value; diff --git a/src/compute/scope.rs b/src/compute/scope.rs new file mode 100644 index 00000000..1fd4db0b --- /dev/null +++ b/src/compute/scope.rs @@ -0,0 +1,50 @@ +//! Mapping from identifiers to functions. + +use std::collections::HashMap; +use std::fmt::{self, Debug, Formatter}; + +use super::value::FuncValue; + +/// A map from identifiers to functions. +pub struct Scope { + functions: HashMap<String, FuncValue>, + fallback: FuncValue, +} + +impl Scope { + // Create a new empty scope with a fallback function that is invoked when no + // match is found. + pub fn new(fallback: FuncValue) -> Self { + Self { + functions: HashMap::new(), + fallback, + } + } + + /// Associate the given name with the function. + pub fn insert(&mut self, name: impl Into<String>, function: FuncValue) { + self.functions.insert(name.into(), function); + } + + /// Return the function with the given name if there is one. + pub fn func(&self, name: &str) -> Option<&FuncValue> { + self.functions.get(name) + } + + /// Return the function with the given name or the fallback if there is no + /// such function. + pub fn func_or_fallback(&self, name: &str) -> &FuncValue { + self.func(name).unwrap_or_else(|| self.fallback()) + } + + /// Return the fallback function. + pub fn fallback(&self) -> &FuncValue { + &self.fallback + } +} + +impl Debug for Scope { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.debug_set().entries(self.functions.keys()).finish() + } +} diff --git a/src/compute/table.rs b/src/compute/table.rs new file mode 100644 index 00000000..f11eacfc --- /dev/null +++ b/src/compute/table.rs @@ -0,0 +1,379 @@ +//! A key-value map that can also model array-like structures. + +use std::collections::BTreeMap; +use std::fmt::{self, Debug, Formatter}; +use std::ops::Index; + +use crate::syntax::span::{Span, Spanned}; + +/// A table data structure, which maps from integers (`u64`) or strings to a +/// generic value type. +/// +/// The table can be used to model arrays by assigns values to successive +/// indices from `0..n`. The table type offers special support for this pattern +/// through the `push` method. +#[derive(Clone)] +pub struct Table<V> { + nums: BTreeMap<u64, V>, + strs: BTreeMap<String, V>, + lowest_free: u64, +} + +impl<V> Table<V> { + /// Create a new empty table. + pub fn new() -> Self { + Self { + nums: BTreeMap::new(), + strs: BTreeMap::new(), + lowest_free: 0, + } + } + + /// The total number of entries in the table. + pub fn len(&self) -> usize { + self.nums.len() + self.strs.len() + } + + /// Whether the table contains no entries. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// The first number key-value pair (with lowest number). + pub fn first(&self) -> Option<(u64, &V)> { + self.nums.iter().next().map(|(&k, v)| (k, v)) + } + + /// The last number key-value pair (with highest number). + pub fn last(&self) -> Option<(u64, &V)> { + self.nums.iter().next_back().map(|(&k, v)| (k, v)) + } + + /// Get a reference to the value with the given key. + pub fn get<'a, K>(&self, key: K) -> Option<&V> + where + K: Into<BorrowedKey<'a>>, + { + match key.into() { + BorrowedKey::Num(num) => self.nums.get(&num), + BorrowedKey::Str(string) => self.strs.get(string), + } + } + + /// Borrow the value with the given key mutably. + pub fn get_mut<'a, K>(&mut self, key: K) -> Option<&mut V> + where + K: Into<BorrowedKey<'a>>, + { + match key.into() { + BorrowedKey::Num(num) => self.nums.get_mut(&num), + BorrowedKey::Str(string) => self.strs.get_mut(string), + } + } + + /// Insert a value into the table. + pub fn insert<K>(&mut self, key: K, value: V) + where + K: Into<OwnedKey>, + { + match key.into() { + OwnedKey::Num(num) => { + self.nums.insert(num, value); + if self.lowest_free == num { + self.lowest_free += 1; + } + } + OwnedKey::Str(string) => { + self.strs.insert(string, value); + } + } + } + + /// Remove the value with the given key from the table. + pub fn remove<'a, K>(&mut self, key: K) -> Option<V> + where + K: Into<BorrowedKey<'a>>, + { + match key.into() { + BorrowedKey::Num(num) => { + self.lowest_free = self.lowest_free.min(num); + self.nums.remove(&num) + } + BorrowedKey::Str(string) => self.strs.remove(string), + } + } + + /// Append a value to the table. + /// + /// This will associate the `value` with the lowest free number key (zero if + /// there is no number key so far). + pub fn push(&mut self, value: V) { + while self.nums.contains_key(&self.lowest_free) { + self.lowest_free += 1; + } + self.nums.insert(self.lowest_free, value); + self.lowest_free += 1; + } + + /// Iterate over the number key-value pairs. + pub fn nums(&self) -> std::collections::btree_map::Iter<u64, V> { + self.nums.iter() + } + + /// Iterate over the string key-value pairs. + pub fn strs(&self) -> std::collections::btree_map::Iter<String, V> { + self.strs.iter() + } + + /// Iterate over all values in the table. + pub fn values(&self) -> impl Iterator<Item = &V> { + self.nums().map(|(_, v)| v).chain(self.strs().map(|(_, v)| v)) + } + + /// Iterate over the number key-value pairs. + pub fn into_nums(self) -> std::collections::btree_map::IntoIter<u64, V> { + self.nums.into_iter() + } + + /// Iterate over the string key-value pairs. + pub fn into_strs(self) -> std::collections::btree_map::IntoIter<String, V> { + self.strs.into_iter() + } + + /// Move into an owned iterator over all values in the table. + pub fn into_values(self) -> impl Iterator<Item = V> { + self.nums.into_iter().map(|(_, v)| v) + .chain(self.strs.into_iter().map(|(_, v)| v)) + } +} + +impl<'a, K, V> Index<K> for Table<V> +where + K: Into<BorrowedKey<'a>>, +{ + type Output = V; + + fn index(&self, index: K) -> &Self::Output { + self.get(index).expect("key not in table") + } +} + +impl<V> Default for Table<V> { + fn default() -> Self { + Self::new() + } +} + +impl<V: Eq> Eq for Table<V> {} + +impl<V: PartialEq> PartialEq for Table<V> { + fn eq(&self, other: &Self) -> bool { + self.nums().eq(other.nums()) && self.strs().eq(other.strs()) + } +} + +impl<V: Debug> Debug for Table<V> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if self.is_empty() { + return f.write_str("()"); + } + + let mut builder = f.debug_tuple(""); + + struct Entry<'a>(&'a dyn Debug, &'a dyn Debug); + impl<'a> Debug for Entry<'a> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.0.fmt(f)?; + if f.alternate() { + f.write_str(" = ")?; + } else { + f.write_str("=")?; + } + self.1.fmt(f) + } + } + + for (key, value) in self.nums() { + builder.field(&Entry(&key, &value)); + } + + for (key, value) in self.strs() { + builder.field(&Entry(&key, &value)); + } + + builder.finish() + } +} + +/// The owned variant of a table key. +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub enum OwnedKey { + Num(u64), + Str(String), +} + +impl From<u64> for OwnedKey { + fn from(num: u64) -> Self { + Self::Num(num) + } +} + +impl From<String> for OwnedKey { + fn from(string: String) -> Self { + Self::Str(string) + } +} + +impl From<&'static str> for OwnedKey { + fn from(string: &'static str) -> Self { + Self::Str(string.to_string()) + } +} + +/// The borrowed variant of a table key. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub enum BorrowedKey<'a> { + Num(u64), + Str(&'a str), +} + +impl From<u64> for BorrowedKey<'static> { + fn from(num: u64) -> Self { + Self::Num(num) + } +} + +impl<'a> From<&'a String> for BorrowedKey<'a> { + fn from(string: &'a String) -> Self { + Self::Str(&string) + } +} + +impl<'a> From<&'a str> for BorrowedKey<'a> { + fn from(string: &'a str) -> Self { + Self::Str(string) + } +} + +/// An table entry which tracks key and value span. +#[derive(Clone, PartialEq)] +pub struct SpannedEntry<V> { + pub key: Span, + pub val: Spanned<V>, +} + +impl<V> SpannedEntry<V> { + /// Create a new entry. + pub fn new(key: Span, val: Spanned<V>) -> Self { + Self { key, val } + } + + /// Create an entry with the same span for key and value. + pub fn val(val: Spanned<V>) -> Self { + Self { key: Span::ZERO, val } + } + + /// Convert from `&SpannedEntry<T>` to `SpannedEntry<&T>` + pub fn as_ref(&self) -> SpannedEntry<&V> { + SpannedEntry { key: self.key, val: self.val.as_ref() } + } + + /// Map the entry to a different value type. + pub fn map<U>(self, f: impl FnOnce(V) -> U) -> SpannedEntry<U> { + SpannedEntry { key: self.key, val: self.val.map(f) } + } +} + +impl<V: Debug> Debug for SpannedEntry<V> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if f.alternate() { + f.write_str("key")?; + self.key.fmt(f)?; + f.write_str(" ")?; + } + self.val.fmt(f) + } +} + +#[cfg(test)] +mod tests { + use super::Table; + + #[test] + fn test_table_different_key_types_dont_interfere() { + let mut table = Table::new(); + table.insert(10, "hello"); + table.insert("twenty", "there"); + assert_eq!(table.len(), 2); + assert_eq!(table[10], "hello"); + assert_eq!(table["twenty"], "there"); + } + + #[test] + fn test_table_push_skips_already_inserted_keys() { + let mut table = Table::new(); + table.insert(2, "2"); + table.push("0"); + table.insert(3, "3"); + table.push("1"); + table.push("4"); + assert_eq!(table.len(), 5); + assert_eq!(table[0], "0"); + assert_eq!(table[1], "1"); + assert_eq!(table[2], "2"); + assert_eq!(table[3], "3"); + assert_eq!(table[4], "4"); + } + + #[test] + fn test_table_push_remove_push_reuses_index() { + let mut table = Table::new(); + table.push("0"); + table.push("1"); + table.push("2"); + table.remove(1); + table.push("a"); + table.push("3"); + assert_eq!(table.len(), 4); + assert_eq!(table[0], "0"); + assert_eq!(table[1], "a"); + assert_eq!(table[2], "2"); + assert_eq!(table[3], "3"); + } + + #[test] + fn test_table_first_and_last_are_correct() { + let mut table = Table::new(); + assert_eq!(table.first(), None); + assert_eq!(table.last(), None); + table.insert(4, "hi"); + table.insert("string", "hi"); + assert_eq!(table.first(), Some((4, &"hi"))); + assert_eq!(table.last(), Some((4, &"hi"))); + table.insert(2, "bye"); + assert_eq!(table.first(), Some((2, &"bye"))); + assert_eq!(table.last(), Some((4, &"hi"))); + } + + #[test] + fn test_table_format_debug() { + let mut table = Table::new(); + assert_eq!(format!("{:?}", table), r#"()"#); + assert_eq!(format!("{:#?}", table), r#"()"#); + + table.insert(10, "hello"); + table.insert("twenty", "there"); + table.insert("sp ace", "quotes"); + assert_eq!( + format!("{:?}", table), + r#"(10="hello", "sp ace"="quotes", "twenty"="there")"#, + ); + assert_eq!(format!("{:#?}", table).lines().collect::<Vec<_>>(), [ + "(", + r#" 10 = "hello","#, + r#" "sp ace" = "quotes","#, + r#" "twenty" = "there","#, + ")", + ]); + } +} diff --git a/src/compute/value.rs b/src/compute/value.rs new file mode 100644 index 00000000..daa3b17b --- /dev/null +++ b/src/compute/value.rs @@ -0,0 +1,474 @@ +//! Computational values: Syntactical expressions can be evaluated into these. + +use std::fmt::{self, Debug, Formatter}; +use std::rc::Rc; + +use fontdock::{FontStyle, FontWeight, FontWidth}; + +use crate::color::RgbaColor; +use crate::layout::{Commands, Dir, LayoutContext, SpecAlign}; +use crate::length::{Length, ScaleLength}; +use crate::paper::Paper; +use crate::syntax::span::{Span, Spanned}; +use crate::syntax::tree::SyntaxTree; +use crate::syntax::Ident; +use crate::{DynFuture, Feedback, Pass}; +use super::table::{BorrowedKey, SpannedEntry, Table}; + +/// A computational value. +#[derive(Clone)] +pub enum Value { + /// An identifier: `ident`. + Ident(Ident), + /// A string: `"string"`. + Str(String), + /// A boolean: `true, false`. + Bool(bool), + /// A number: `1.2, 200%`. + Number(f64), + /// A length: `2cm, 5.2in`. + Length(Length), + /// A color value with alpha channel: `#f79143ff`. + Color(RgbaColor), + /// A table value: `(false, 12cm, greeting="hi")`. + Table(TableValue), + /// A syntax tree containing typesetting content. + Tree(SyntaxTree), + /// An executable function. + Func(FuncValue), + /// Layouting commands. + Commands(Commands), +} + +impl Value { + /// A natural-language name of the type of this expression, e.g. + /// "identifier". + pub fn name(&self) -> &'static str { + use Value::*; + match self { + Ident(_) => "identifier", + Str(_) => "string", + Bool(_) => "bool", + Number(_) => "number", + Length(_) => "length", + Color(_) => "color", + Table(_) => "table", + Tree(_) => "syntax tree", + Func(_) => "function", + Commands(_) => "commands", + } + } +} + +impl Debug for Value { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + use Value::*; + match self { + Ident(i) => i.fmt(f), + Str(s) => s.fmt(f), + Bool(b) => b.fmt(f), + Number(n) => n.fmt(f), + Length(s) => s.fmt(f), + Color(c) => c.fmt(f), + Table(t) => t.fmt(f), + Tree(t) => t.fmt(f), + Func(_) => f.pad("<function>"), + Commands(c) => c.fmt(f), + } + } +} + +impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + use Value::*; + match (self, other) { + (Ident(a), Ident(b)) => a == b, + (Str(a), Str(b)) => a == b, + (Bool(a), Bool(b)) => a == b, + (Number(a), Number(b)) => a == b, + (Length(a), Length(b)) => a == b, + (Color(a), Color(b)) => a == b, + (Table(a), Table(b)) => a == b, + (Tree(a), Tree(b)) => a == b, + (Func(a), Func(b)) => { + a.as_ref() as *const _ == b.as_ref() as *const _ + } + (Commands(a), Commands(b)) => a == b, + _ => false, + } + } +} + +/// An executable function value. +/// +/// The first argument is a table containing the arguments passed to the +/// function. The function may be asynchronous (as such it returns a dynamic +/// future) and it may emit diagnostics, which are contained in the returned +/// `Pass`. In the end, the function must evaluate to `Value`. Your typical +/// typesetting function will return a `Commands` value which will instruct the +/// layouting engine to do what the function pleases. +/// +/// The dynamic function object is wrapped in an `Rc` to keep `Value` clonable. +pub type FuncValue = Rc< + dyn Fn(TableValue, LayoutContext<'_>) -> DynFuture<Pass<Value>> +>; + +/// A table of values. +/// +/// # Example +/// ```typst +/// (false, 12cm, greeting="hi") +/// ``` +pub type TableValue = Table<SpannedEntry<Value>>; + +impl TableValue { + /// Retrieve and remove the matching value with the lowest number key, + /// skipping and ignoring all non-matching entries with lower keys. + pub fn take<T: TryFromValue>(&mut self) -> Option<T> { + for (&key, entry) in self.nums() { + let expr = entry.val.as_ref(); + if let Some(val) = T::try_from_value(expr, &mut Feedback::new()) { + self.remove(key); + return Some(val); + } + } + None + } + + /// Retrieve and remove the matching value with the lowest number key, + /// removing and generating errors for all non-matching entries with lower + /// keys. + pub fn expect<T: TryFromValue>(&mut self, f: &mut Feedback) -> Option<T> { + while let Some((num, _)) = self.first() { + let entry = self.remove(num).unwrap(); + if let Some(val) = T::try_from_value(entry.val.as_ref(), f) { + return Some(val); + } + } + None + } + + /// Retrieve and remove a matching value associated with the given key if + /// there is any. + /// + /// Generates an error if the key exists but the value does not match. + pub fn take_with_key<'a, K, T>(&mut self, key: K, f: &mut Feedback) -> Option<T> + where + K: Into<BorrowedKey<'a>>, + T: TryFromValue, + { + self.remove(key).and_then(|entry| { + let expr = entry.val.as_ref(); + T::try_from_value(expr, f) + }) + } + + /// Retrieve and remove all matching pairs with number keys, skipping and + /// ignoring non-matching entries. + /// + /// The pairs are returned in order of increasing keys. + pub fn take_all_num<'a, T>(&'a mut self) -> impl Iterator<Item = (u64, T)> + 'a + where + T: TryFromValue, + { + let mut skip = 0; + std::iter::from_fn(move || { + for (&key, entry) in self.nums().skip(skip) { + let expr = entry.val.as_ref(); + if let Some(val) = T::try_from_value(expr, &mut Feedback::new()) { + self.remove(key); + return Some((key, val)); + } + skip += 1; + } + + None + }) + } + + + /// Retrieve and remove all matching values with number keys, skipping and + /// ignoring non-matching entries. + /// + /// The values are returned in order of increasing keys. + pub fn take_all_num_vals<'a, T: 'a>(&'a mut self) -> impl Iterator<Item = T> + 'a + where + T: TryFromValue, + { + self.take_all_num::<T>().map(|(_, v)| v) + } + + /// Retrieve and remove all matching pairs with string keys, skipping and + /// ignoring non-matching entries. + /// + /// The pairs are returned in order of increasing keys. + pub fn take_all_str<'a, T>(&'a mut self) -> impl Iterator<Item = (String, T)> + 'a + where + T: TryFromValue, + { + let mut skip = 0; + std::iter::from_fn(move || { + for (key, entry) in self.strs().skip(skip) { + let expr = entry.val.as_ref(); + if let Some(val) = T::try_from_value(expr, &mut Feedback::new()) { + let key = key.clone(); + self.remove(&key); + return Some((key, val)); + } + skip += 1; + } + + None + }) + } + + /// Generated `"unexpected argument"` errors for all remaining entries. + pub fn unexpected(&self, f: &mut Feedback) { + for entry in self.values() { + let span = Span::merge(entry.key, entry.val.span); + error!(@f, span, "unexpected argument"); + } + } +} + +/// A trait for converting values into more specific types. +pub trait TryFromValue: Sized { + // This trait takes references because we don't want to move the value + // out of its origin in case this returns `None`. This solution is not + // perfect because we need to do some cloning in the impls for this trait, + // but we haven't got a better solution, for now. + + /// Try to convert a value to this type. + /// + /// Returns `None` and generates an appropriate error if the value is not + /// valid for this type. + fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option<Self>; +} + +macro_rules! impl_match { + ($type:ty, $name:expr, $($p:pat => $r:expr),* $(,)?) => { + impl TryFromValue for $type { + fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option<Self> { + #[allow(unreachable_patterns)] + match value.v { + $($p => Some($r)),*, + other => { + error!( + @f, value.span, + "expected {}, found {}", $name, other.name() + ); + None + } + } + } + } + }; +} + +macro_rules! impl_ident { + ($type:ty, $name:expr, $parse:expr) => { + impl TryFromValue for $type { + fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option<Self> { + if let Value::Ident(ident) = value.v { + let val = $parse(ident.as_str()); + if val.is_none() { + error!(@f, value.span, "invalid {}", $name); + } + val + } else { + error!( + @f, value.span, + "expected {}, found {}", $name, value.v.name() + ); + None + } + } + } + }; +} + +impl<T: TryFromValue> TryFromValue for Spanned<T> { + fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option<Self> { + let span = value.span; + T::try_from_value(value, f).map(|v| Spanned { v, span }) + } +} + +impl_match!(Value, "value", v => v.clone()); +impl_match!(Ident, "identifier", Value::Ident(i) => i.clone()); +impl_match!(String, "string", Value::Str(s) => s.clone()); +impl_match!(bool, "bool", &Value::Bool(b) => b); +impl_match!(f64, "number", &Value::Number(n) => n); +impl_match!(Length, "length", &Value::Length(l) => l); +impl_match!(SyntaxTree, "tree", Value::Tree(t) => t.clone()); +impl_match!(TableValue, "table", Value::Table(t) => t.clone()); +impl_match!(FuncValue, "function", Value::Func(f) => f.clone()); +impl_match!(ScaleLength, "number or length", + &Value::Length(length) => ScaleLength::Absolute(length), + &Value::Number(scale) => ScaleLength::Scaled(scale), +); + +/// A value type that matches identifiers and strings and implements +/// `Into<String>`. +pub struct StringLike(pub String); + +impl From<StringLike> for String { + fn from(like: StringLike) -> String { + like.0 + } +} + +impl_match!(StringLike, "identifier or string", + Value::Ident(Ident(s)) => StringLike(s.clone()), + Value::Str(s) => StringLike(s.clone()), +); + +impl_ident!(Dir, "direction", |s| match s { + "ltr" => Some(Self::LTR), + "rtl" => Some(Self::RTL), + "ttb" => Some(Self::TTB), + "btt" => Some(Self::BTT), + _ => None, +}); + +impl_ident!(SpecAlign, "alignment", |s| match s { + "left" => Some(Self::Left), + "right" => Some(Self::Right), + "top" => Some(Self::Top), + "bottom" => Some(Self::Bottom), + "center" => Some(Self::Center), + _ => None, +}); + +impl_ident!(FontStyle, "font style", FontStyle::from_name); +impl_ident!(Paper, "paper", Paper::from_name); + +impl TryFromValue for FontWeight { + fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option<Self> { + match value.v { + &Value::Number(weight) => { + const MIN: u16 = 100; + const MAX: u16 = 900; + + Some(Self(if weight < MIN as f64 { + error!(@f, value.span, "the minimum font weight is {}", MIN); + MIN + } else if weight > MAX as f64 { + error!(@f, value.span, "the maximum font weight is {}", MAX); + MAX + } else { + weight.round() as u16 + })) + } + Value::Ident(ident) => { + let weight = Self::from_name(ident.as_str()); + if weight.is_none() { + error!(@f, value.span, "invalid font weight"); + } + weight + } + other => { + error!( + @f, value.span, + "expected font weight (name or number), found {}", + other.name(), + ); + None + } + } + } +} + +impl TryFromValue for FontWidth { + fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option<Self> { + match value.v { + &Value::Number(width) => { + const MIN: u16 = 1; + const MAX: u16 = 9; + + Self::new(if width < MIN as f64 { + error!(@f, value.span, "the minimum font width is {}", MIN); + MIN + } else if width > MAX as f64 { + error!(@f, value.span, "the maximum font width is {}", MAX); + MAX + } else { + width.round() as u16 + }) + } + Value::Ident(ident) => { + let width = Self::from_name(ident.as_str()); + if width.is_none() { + error!(@f, value.span, "invalid font width"); + } + width + } + other => { + error!( + @f, value.span, + "expected font width (name or number), found {}", + other.name(), + ); + None + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn entry(value: Value) -> SpannedEntry<Value> { + SpannedEntry::val(Spanned::zero(value)) + } + + #[test] + fn test_table_take_removes_correct_entry() { + let mut table = Table::new(); + table.insert(1, entry(Value::Bool(false))); + table.insert(2, entry(Value::Str("hi".to_string()))); + assert_eq!(table.take::<String>(), Some("hi".to_string())); + assert_eq!(table.len(), 1); + assert_eq!(table.take::<bool>(), Some(false)); + assert!(table.is_empty()); + } + + #[test] + fn test_table_expect_errors_about_previous_entries() { + let mut f = Feedback::new(); + let mut table = Table::new(); + table.insert(1, entry(Value::Bool(false))); + table.insert(3, entry(Value::Str("hi".to_string()))); + table.insert(5, entry(Value::Bool(true))); + assert_eq!(table.expect::<String>(&mut f), Some("hi".to_string())); + assert_eq!(f.diagnostics, [error!(Span::ZERO, "expected string, found bool")]); + assert_eq!(table.len(), 1); + } + + #[test] + fn test_table_take_with_key_removes_the_entry() { + let mut f = Feedback::new(); + let mut table = Table::new(); + table.insert(1, entry(Value::Bool(false))); + table.insert("hi", entry(Value::Bool(true))); + assert_eq!(table.take_with_key::<_, bool>(1, &mut f), Some(false)); + assert_eq!(table.take_with_key::<_, f64>("hi", &mut f), None); + assert_eq!(f.diagnostics, [error!(Span::ZERO, "expected number, found bool")]); + assert!(table.is_empty()); + } + + #[test] + fn test_table_take_all_removes_the_correct_entries() { + let mut table = Table::new(); + table.insert(1, entry(Value::Bool(false))); + table.insert(3, entry(Value::Number(0.0))); + table.insert(7, entry(Value::Bool(true))); + assert_eq!( + table.take_all_num::<bool>().collect::<Vec<_>>(), + [(1, false), (7, true)], + ); + assert_eq!(table.len(), 1); + assert_eq!(table[3].val.v, Value::Number(0.0)); + } +} |
