mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
Dynamic values, Types, Arrays, and Dictionaries 🚀
- Identifiers are now evaluated as variables instead of being plain values - Constants like `left` or `bold` are stored as dynamic values containing the respective rust types - We now distinguish between arrays and dictionaries to make things more intuitive (at the cost of a bit more complex parsing) - Spans were removed from collections (arrays, dictionaries), function arguments still have spans for the top-level values to enable good diagnostics
This commit is contained in:
parent
8cad78481c
commit
1c40dc42e7
@ -111,7 +111,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_color_strings() {
|
||||
fn test_parse_color_strings() {
|
||||
fn test(hex: &str, r: u8, g: u8, b: u8, a: u8) {
|
||||
assert_eq!(RgbaColor::from_str(hex), Ok(RgbaColor::new(r, g, b, a)));
|
||||
}
|
||||
@ -124,7 +124,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_invalid_colors() {
|
||||
fn test_parse_invalid_colors() {
|
||||
fn test(hex: &str) {
|
||||
assert_eq!(RgbaColor::from_str(hex), Err(ParseRgbaError));
|
||||
}
|
||||
|
270
src/eval/args.rs
270
src/eval/args.rs
@ -1,184 +1,138 @@
|
||||
//! Simplifies argument parsing.
|
||||
use super::*;
|
||||
|
||||
use std::mem;
|
||||
/// Evaluated arguments to a function.
|
||||
#[derive(Debug)]
|
||||
pub struct Args {
|
||||
span: Span,
|
||||
pos: SpanVec<Value>,
|
||||
named: Vec<(Spanned<String>, Spanned<Value>)>,
|
||||
}
|
||||
|
||||
use super::{Conv, EvalContext, RefKey, TryFromValue, Value, ValueDict};
|
||||
use crate::diag::Diag;
|
||||
use crate::syntax::{Span, SpanVec, Spanned, WithSpan};
|
||||
impl Eval for Spanned<&Arguments> {
|
||||
type Output = Args;
|
||||
|
||||
/// A wrapper around a dictionary value that simplifies argument parsing in
|
||||
/// functions.
|
||||
pub struct Args(pub Spanned<ValueDict>);
|
||||
fn eval(self, ctx: &mut EvalContext) -> Self::Output {
|
||||
let mut pos = vec![];
|
||||
let mut named = vec![];
|
||||
|
||||
for arg in self.v {
|
||||
match arg {
|
||||
Argument::Pos(expr) => {
|
||||
pos.push(expr.as_ref().eval(ctx).with_span(expr.span));
|
||||
}
|
||||
Argument::Named(Named { name, expr }) => {
|
||||
named.push((
|
||||
name.as_ref().map(|id| id.0.clone()),
|
||||
expr.as_ref().eval(ctx).with_span(expr.span),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Args { span: self.span, pos, named }
|
||||
}
|
||||
}
|
||||
|
||||
impl Args {
|
||||
/// Retrieve and remove the argument associated with the given key if there
|
||||
/// is any.
|
||||
/// Find the first convertible positional argument.
|
||||
pub fn find<T>(&mut self, ctx: &mut EvalContext) -> Option<T>
|
||||
where
|
||||
T: Cast<Spanned<Value>>,
|
||||
{
|
||||
self.pos.iter_mut().find_map(move |slot| try_cast(ctx, slot))
|
||||
}
|
||||
|
||||
/// Find the first convertible positional argument, producing an error if
|
||||
/// no match was found.
|
||||
pub fn require<T>(&mut self, ctx: &mut EvalContext, what: &str) -> Option<T>
|
||||
where
|
||||
T: Cast<Spanned<Value>>,
|
||||
{
|
||||
let found = self.find(ctx);
|
||||
if found.is_none() {
|
||||
ctx.diag(error!(self.span, "missing argument: {}", what));
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
/// Filter out all convertible positional arguments.
|
||||
pub fn filter<'a, T>(
|
||||
&'a mut self,
|
||||
ctx: &'a mut EvalContext,
|
||||
) -> impl Iterator<Item = T> + 'a
|
||||
where
|
||||
T: Cast<Spanned<Value>>,
|
||||
{
|
||||
self.pos.iter_mut().filter_map(move |slot| try_cast(ctx, slot))
|
||||
}
|
||||
|
||||
/// Convert the value for the given named argument.
|
||||
///
|
||||
/// Generates an error if the key exists, but the value can't be converted
|
||||
/// into the type `T`.
|
||||
pub fn get<'a, K, T>(&mut self, ctx: &mut EvalContext, key: K) -> Option<T>
|
||||
/// Generates an error if the conversion fails.
|
||||
pub fn get<'a, T>(&mut self, ctx: &mut EvalContext, name: &str) -> Option<T>
|
||||
where
|
||||
K: Into<RefKey<'a>>,
|
||||
T: TryFromValue,
|
||||
T: Cast<Spanned<Value>>,
|
||||
{
|
||||
self.0.v.remove(key).and_then(|entry| {
|
||||
let span = entry.value.span;
|
||||
conv_diag(
|
||||
T::try_from_value(entry.value),
|
||||
&mut ctx.feedback.diags,
|
||||
span,
|
||||
)
|
||||
})
|
||||
let index = self.named.iter().position(|(k, _)| k.v.as_str() == name)?;
|
||||
let value = self.named.remove(index).1;
|
||||
cast(ctx, value)
|
||||
}
|
||||
|
||||
/// This is the same as [`get`](Self::get), except that it generates an error about a
|
||||
/// missing argument with the given `name` if the key does not exist.
|
||||
pub fn need<'a, K, T>(
|
||||
&mut self,
|
||||
ctx: &mut EvalContext,
|
||||
key: K,
|
||||
name: &str,
|
||||
) -> Option<T>
|
||||
where
|
||||
K: Into<RefKey<'a>>,
|
||||
T: TryFromValue,
|
||||
{
|
||||
if let Some(entry) = self.0.v.remove(key) {
|
||||
let span = entry.value.span;
|
||||
conv_diag(
|
||||
T::try_from_value(entry.value),
|
||||
&mut ctx.feedback.diags,
|
||||
span,
|
||||
)
|
||||
} else {
|
||||
ctx.diag(error!(self.0.span, "missing argument: {}", name));
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve and remove the first matching positional argument.
|
||||
pub fn find<T>(&mut self) -> Option<T>
|
||||
where
|
||||
T: TryFromValue,
|
||||
{
|
||||
for (&key, entry) in self.0.v.nums_mut() {
|
||||
let span = entry.value.span;
|
||||
let slot = &mut entry.value;
|
||||
let conv = conv_put_back(T::try_from_value(mem::take(slot)), slot, span);
|
||||
if let Some(t) = conv {
|
||||
self.0.v.remove(key);
|
||||
return Some(t);
|
||||
/// Generate "unexpected argument" errors for all remaining arguments.
|
||||
pub fn finish(self, ctx: &mut EvalContext) {
|
||||
let a = self.pos.iter().map(|v| v.as_ref());
|
||||
let b = self.named.iter().map(|(k, v)| (&v.v).with_span(k.span.join(v.span)));
|
||||
for value in a.chain(b) {
|
||||
if value.v != &Value::Error {
|
||||
ctx.diag(error!(value.span, "unexpected argument"));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Retrieve and remove all matching positional arguments.
|
||||
pub fn find_all<T>(&mut self) -> impl Iterator<Item = T> + '_
|
||||
where
|
||||
T: TryFromValue,
|
||||
{
|
||||
let mut skip = 0;
|
||||
std::iter::from_fn(move || {
|
||||
for (&key, entry) in self.0.v.nums_mut().skip(skip) {
|
||||
let span = entry.value.span;
|
||||
let slot = &mut entry.value;
|
||||
let conv = conv_put_back(T::try_from_value(mem::take(slot)), slot, span);
|
||||
if let Some(t) = conv {
|
||||
self.0.v.remove(key);
|
||||
return Some(t);
|
||||
}
|
||||
skip += 1;
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieve and remove all matching named arguments.
|
||||
pub fn find_all_str<T>(&mut self) -> impl Iterator<Item = (String, T)> + '_
|
||||
where
|
||||
T: TryFromValue,
|
||||
{
|
||||
let mut skip = 0;
|
||||
std::iter::from_fn(move || {
|
||||
for (key, entry) in self.0.v.strs_mut().skip(skip) {
|
||||
let span = entry.value.span;
|
||||
let slot = &mut entry.value;
|
||||
let conv = conv_put_back(T::try_from_value(mem::take(slot)), slot, span);
|
||||
if let Some(t) = conv {
|
||||
let key = key.clone();
|
||||
self.0.v.remove(&key);
|
||||
return Some((key, t));
|
||||
}
|
||||
skip += 1;
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Generated _unexpected argument_ errors for all remaining entries.
|
||||
pub fn done(&self, ctx: &mut EvalContext) {
|
||||
for entry in self.0.v.values() {
|
||||
let span = entry.key_span.join(entry.value.span);
|
||||
ctx.diag(error!(span, "unexpected argument"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn conv_diag<T>(conv: Conv<T>, diags: &mut SpanVec<Diag>, span: Span) -> Option<T> {
|
||||
match conv {
|
||||
Conv::Ok(t) => Some(t),
|
||||
Conv::Warn(t, warn) => {
|
||||
diags.push(warn.with_span(span));
|
||||
/// Cast the value into `T`, generating an error if the conversion fails.
|
||||
fn cast<T>(ctx: &mut EvalContext, value: Spanned<Value>) -> Option<T>
|
||||
where
|
||||
T: Cast<Spanned<Value>>,
|
||||
{
|
||||
let span = value.span;
|
||||
match T::cast(value) {
|
||||
CastResult::Ok(t) => Some(t),
|
||||
CastResult::Warn(t, m) => {
|
||||
ctx.diag(warning!(span, "{}", m));
|
||||
Some(t)
|
||||
}
|
||||
Conv::Err(_, err) => {
|
||||
diags.push(err.with_span(span));
|
||||
CastResult::Err(value) => {
|
||||
ctx.diag(error!(
|
||||
span,
|
||||
"expected {}, found {}",
|
||||
T::TYPE_NAME,
|
||||
value.v.type_name()
|
||||
));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn conv_put_back<T>(conv: Conv<T>, slot: &mut Spanned<Value>, span: Span) -> Option<T> {
|
||||
match conv {
|
||||
Conv::Ok(t) => Some(t),
|
||||
Conv::Warn(t, _) => Some(t),
|
||||
Conv::Err(v, _) => {
|
||||
*slot = v.with_span(span);
|
||||
/// Try to cast the value in the slot into `T`, putting it back if the
|
||||
/// conversion fails.
|
||||
fn try_cast<T>(ctx: &mut EvalContext, slot: &mut Spanned<Value>) -> Option<T>
|
||||
where
|
||||
T: Cast<Spanned<Value>>,
|
||||
{
|
||||
// Replace with error placeholder when conversion works since error values
|
||||
// are ignored when generating "unexpected argument" errors.
|
||||
let value = std::mem::replace(slot, Spanned::zero(Value::Error));
|
||||
let span = value.span;
|
||||
match T::cast(value) {
|
||||
CastResult::Ok(t) => Some(t),
|
||||
CastResult::Warn(t, m) => {
|
||||
ctx.diag(warning!(span, "{}", m));
|
||||
Some(t)
|
||||
}
|
||||
CastResult::Err(value) => {
|
||||
*slot = value;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::{Dict, SpannedEntry, Value};
|
||||
use super::*;
|
||||
|
||||
fn entry(value: Value) -> SpannedEntry<Value> {
|
||||
SpannedEntry::value(Spanned::zero(value))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_args_find() {
|
||||
let mut args = Args(Spanned::zero(Dict::new()));
|
||||
args.0.v.insert(1, entry(Value::Bool(false)));
|
||||
args.0.v.insert(2, entry(Value::Str("hi".to_string())));
|
||||
assert_eq!(args.find::<String>(), Some("hi".to_string()));
|
||||
assert_eq!(args.0.v.len(), 1);
|
||||
assert_eq!(args.find::<bool>(), Some(false));
|
||||
assert!(args.0.v.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_args_find_all() {
|
||||
let mut args = Args(Spanned::zero(Dict::new()));
|
||||
args.0.v.insert(1, entry(Value::Bool(false)));
|
||||
args.0.v.insert(3, entry(Value::Float(0.0)));
|
||||
args.0.v.insert(7, entry(Value::Bool(true)));
|
||||
assert_eq!(args.find_all::<bool>().collect::<Vec<_>>(), [false, true]);
|
||||
assert_eq!(args.0.v.len(), 1);
|
||||
assert_eq!(args.0.v[3].value.v, Value::Float(0.0));
|
||||
}
|
||||
}
|
||||
|
522
src/eval/dict.rs
522
src/eval/dict.rs
@ -1,522 +0,0 @@
|
||||
//! A key-value map that can also model array-like structures.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::{self, Debug, Display, Formatter};
|
||||
use std::iter::{Extend, FromIterator};
|
||||
use std::ops::Index;
|
||||
|
||||
use crate::syntax::{Span, Spanned};
|
||||
|
||||
/// A dictionary data structure, which maps from integers and strings to a
|
||||
/// generic value type.
|
||||
///
|
||||
/// The dictionary can be used to model arrays by assigning values to successive
|
||||
/// indices from `0..n`. The `push` method offers special support for this
|
||||
/// pattern.
|
||||
#[derive(Clone)]
|
||||
pub struct Dict<V> {
|
||||
nums: BTreeMap<u64, V>,
|
||||
strs: BTreeMap<String, V>,
|
||||
lowest_free: u64,
|
||||
}
|
||||
|
||||
impl<V> Dict<V> {
|
||||
/// Create a new empty dictionary.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
nums: BTreeMap::new(),
|
||||
strs: BTreeMap::new(),
|
||||
lowest_free: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// The total number of entries in the dictionary.
|
||||
pub fn len(&self) -> usize {
|
||||
self.nums.len() + self.strs.len()
|
||||
}
|
||||
|
||||
/// Whether the dictionary 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<RefKey<'a>>,
|
||||
{
|
||||
match key.into() {
|
||||
RefKey::Num(num) => self.nums.get(&num),
|
||||
RefKey::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<RefKey<'a>>,
|
||||
{
|
||||
match key.into() {
|
||||
RefKey::Num(num) => self.nums.get_mut(&num),
|
||||
RefKey::Str(string) => self.strs.get_mut(string),
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a value into the dictionary.
|
||||
pub fn insert<K>(&mut self, key: K, value: V)
|
||||
where
|
||||
K: Into<DictKey>,
|
||||
{
|
||||
match key.into() {
|
||||
DictKey::Num(num) => {
|
||||
self.nums.insert(num, value);
|
||||
if self.lowest_free == num {
|
||||
self.lowest_free += 1;
|
||||
}
|
||||
}
|
||||
DictKey::Str(string) => {
|
||||
self.strs.insert(string, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the value with the given key from the dictionary.
|
||||
pub fn remove<'a, K>(&mut self, key: K) -> Option<V>
|
||||
where
|
||||
K: Into<RefKey<'a>>,
|
||||
{
|
||||
match key.into() {
|
||||
RefKey::Num(num) => {
|
||||
self.lowest_free = self.lowest_free.min(num);
|
||||
self.nums.remove(&num)
|
||||
}
|
||||
RefKey::Str(string) => self.strs.remove(string),
|
||||
}
|
||||
}
|
||||
|
||||
/// Append a value to the dictionary.
|
||||
///
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, K, V> Index<K> for Dict<V>
|
||||
where
|
||||
K: Into<RefKey<'a>>,
|
||||
{
|
||||
type Output = V;
|
||||
|
||||
fn index(&self, index: K) -> &Self::Output {
|
||||
self.get(index).expect("key not in dict")
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: Eq> Eq for Dict<V> {}
|
||||
|
||||
impl<V: PartialEq> PartialEq for Dict<V> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.iter().eq(other.iter())
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Default for Dict<V> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: Debug> Debug for Dict<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>(bool, &'a dyn Display, &'a dyn Debug);
|
||||
impl<'a> Debug for Entry<'a> {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
if self.0 {
|
||||
f.write_str("\"")?;
|
||||
}
|
||||
self.1.fmt(f)?;
|
||||
if self.0 {
|
||||
f.write_str("\"")?;
|
||||
}
|
||||
|
||||
f.write_str(": ")?;
|
||||
|
||||
self.2.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
for (key, value) in self.nums() {
|
||||
builder.field(&Entry(false, &key, &value));
|
||||
}
|
||||
|
||||
for (key, value) in self.strs() {
|
||||
builder.field(&Entry(key.contains(' '), &key, &value));
|
||||
}
|
||||
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Iteration.
|
||||
impl<V> Dict<V> {
|
||||
/// Iterator over all borrowed keys and values.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (RefKey, &V)> {
|
||||
self.into_iter()
|
||||
}
|
||||
|
||||
/// Iterator over all borrowed keys and values.
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = (RefKey, &mut V)> {
|
||||
self.into_iter()
|
||||
}
|
||||
|
||||
/// Iterate over all values in the dictionary.
|
||||
pub fn values(&self) -> impl Iterator<Item = &V> {
|
||||
self.nums().map(|(_, v)| v).chain(self.strs().map(|(_, v)| v))
|
||||
}
|
||||
|
||||
/// Iterate over all values in the dictionary.
|
||||
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut V> {
|
||||
self.nums
|
||||
.iter_mut()
|
||||
.map(|(_, v)| v)
|
||||
.chain(self.strs.iter_mut().map(|(_, v)| v))
|
||||
}
|
||||
|
||||
/// Move into an owned iterator over all values in the dictionary.
|
||||
pub fn into_values(self) -> impl Iterator<Item = V> {
|
||||
self.nums
|
||||
.into_iter()
|
||||
.map(|(_, v)| v)
|
||||
.chain(self.strs.into_iter().map(|(_, v)| v))
|
||||
}
|
||||
|
||||
/// Iterate over the number key-value pairs.
|
||||
pub fn nums(&self) -> std::collections::btree_map::Iter<u64, V> {
|
||||
self.nums.iter()
|
||||
}
|
||||
|
||||
/// Iterate mutably over the number key-value pairs.
|
||||
pub fn nums_mut(&mut self) -> std::collections::btree_map::IterMut<u64, V> {
|
||||
self.nums.iter_mut()
|
||||
}
|
||||
|
||||
/// 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 strs(&self) -> std::collections::btree_map::Iter<String, V> {
|
||||
self.strs.iter()
|
||||
}
|
||||
|
||||
/// Iterate mutably over the string key-value pairs.
|
||||
pub fn strs_mut(&mut self) -> std::collections::btree_map::IterMut<String, V> {
|
||||
self.strs.iter_mut()
|
||||
}
|
||||
|
||||
/// Iterate over the string key-value pairs.
|
||||
pub fn into_strs(self) -> std::collections::btree_map::IntoIter<String, V> {
|
||||
self.strs.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Extend<(DictKey, V)> for Dict<V> {
|
||||
fn extend<T: IntoIterator<Item = (DictKey, V)>>(&mut self, iter: T) {
|
||||
for (key, value) in iter.into_iter() {
|
||||
self.insert(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> FromIterator<(DictKey, V)> for Dict<V> {
|
||||
fn from_iter<T: IntoIterator<Item = (DictKey, V)>>(iter: T) -> Self {
|
||||
let mut v = Self::new();
|
||||
v.extend(iter);
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> IntoIterator for Dict<V> {
|
||||
type Item = (DictKey, V);
|
||||
type IntoIter = std::iter::Chain<
|
||||
std::iter::Map<
|
||||
std::collections::btree_map::IntoIter<u64, V>,
|
||||
fn((u64, V)) -> (DictKey, V),
|
||||
>,
|
||||
std::iter::Map<
|
||||
std::collections::btree_map::IntoIter<String, V>,
|
||||
fn((String, V)) -> (DictKey, V),
|
||||
>,
|
||||
>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
let nums = self.nums.into_iter().map((|(k, v)| (DictKey::Num(k), v)) as _);
|
||||
let strs = self.strs.into_iter().map((|(k, v)| (DictKey::Str(k), v)) as _);
|
||||
nums.chain(strs)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, V> IntoIterator for &'a Dict<V> {
|
||||
type Item = (RefKey<'a>, &'a V);
|
||||
type IntoIter = std::iter::Chain<
|
||||
std::iter::Map<
|
||||
std::collections::btree_map::Iter<'a, u64, V>,
|
||||
fn((&'a u64, &'a V)) -> (RefKey<'a>, &'a V),
|
||||
>,
|
||||
std::iter::Map<
|
||||
std::collections::btree_map::Iter<'a, String, V>,
|
||||
fn((&'a String, &'a V)) -> (RefKey<'a>, &'a V),
|
||||
>,
|
||||
>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
let nums = self.nums().map((|(k, v): (&u64, _)| (RefKey::Num(*k), v)) as _);
|
||||
let strs = self.strs().map((|(k, v): (&'a String, _)| (RefKey::Str(k), v)) as _);
|
||||
nums.chain(strs)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, V> IntoIterator for &'a mut Dict<V> {
|
||||
type Item = (RefKey<'a>, &'a mut V);
|
||||
type IntoIter = std::iter::Chain<
|
||||
std::iter::Map<
|
||||
std::collections::btree_map::IterMut<'a, u64, V>,
|
||||
fn((&'a u64, &'a mut V)) -> (RefKey<'a>, &'a mut V),
|
||||
>,
|
||||
std::iter::Map<
|
||||
std::collections::btree_map::IterMut<'a, String, V>,
|
||||
fn((&'a String, &'a mut V)) -> (RefKey<'a>, &'a mut V),
|
||||
>,
|
||||
>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
let nums = self
|
||||
.nums
|
||||
.iter_mut()
|
||||
.map((|(k, v): (&u64, _)| (RefKey::Num(*k), v)) as _);
|
||||
let strs = self
|
||||
.strs
|
||||
.iter_mut()
|
||||
.map((|(k, v): (&'a String, _)| (RefKey::Str(k), v)) as _);
|
||||
nums.chain(strs)
|
||||
}
|
||||
}
|
||||
|
||||
/// The owned variant of a dictionary key.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum DictKey {
|
||||
Num(u64),
|
||||
Str(String),
|
||||
}
|
||||
|
||||
impl From<&Self> for DictKey {
|
||||
fn from(key: &Self) -> Self {
|
||||
key.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RefKey<'_>> for DictKey {
|
||||
fn from(key: RefKey<'_>) -> Self {
|
||||
match key {
|
||||
RefKey::Num(num) => Self::Num(num),
|
||||
RefKey::Str(string) => Self::Str(string.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for DictKey {
|
||||
fn from(num: u64) -> Self {
|
||||
Self::Num(num)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for DictKey {
|
||||
fn from(string: String) -> Self {
|
||||
Self::Str(string)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for DictKey {
|
||||
fn from(string: &'static str) -> Self {
|
||||
Self::Str(string.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// The borrowed variant of a dictionary key.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum RefKey<'a> {
|
||||
Num(u64),
|
||||
Str(&'a str),
|
||||
}
|
||||
|
||||
impl From<u64> for RefKey<'static> {
|
||||
fn from(num: u64) -> Self {
|
||||
Self::Num(num)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a String> for RefKey<'a> {
|
||||
fn from(string: &'a String) -> Self {
|
||||
Self::Str(&string)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for RefKey<'a> {
|
||||
fn from(string: &'a str) -> Self {
|
||||
Self::Str(string)
|
||||
}
|
||||
}
|
||||
|
||||
/// A dictionary entry which combines key span and value.
|
||||
///
|
||||
/// This exists because a key in a directory can't track its span by itself.
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct SpannedEntry<V> {
|
||||
pub key_span: Span,
|
||||
pub value: Spanned<V>,
|
||||
}
|
||||
|
||||
impl<V> SpannedEntry<V> {
|
||||
/// Create a new entry.
|
||||
pub fn new(key: Span, val: Spanned<V>) -> Self {
|
||||
Self { key_span: key, value: val }
|
||||
}
|
||||
|
||||
/// Create an entry with the same span for key and value.
|
||||
pub fn value(val: Spanned<V>) -> Self {
|
||||
Self { key_span: val.span, value: val }
|
||||
}
|
||||
|
||||
/// Convert from `&SpannedEntry<T>` to `SpannedEntry<&T>`
|
||||
pub fn as_ref(&self) -> SpannedEntry<&V> {
|
||||
SpannedEntry {
|
||||
key_span: self.key_span,
|
||||
value: self.value.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Map the entry to a different value type.
|
||||
pub fn map<U>(self, f: impl FnOnce(V) -> U) -> SpannedEntry<U> {
|
||||
SpannedEntry {
|
||||
key_span: self.key_span,
|
||||
value: self.value.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_span.fmt(f)?;
|
||||
f.write_str(" ")?;
|
||||
}
|
||||
self.value.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Dict;
|
||||
|
||||
#[test]
|
||||
fn test_dict_different_key_types_dont_interfere() {
|
||||
let mut dict = Dict::new();
|
||||
dict.insert(10, "hello");
|
||||
dict.insert("twenty", "there");
|
||||
assert_eq!(dict.len(), 2);
|
||||
assert_eq!(dict[10], "hello");
|
||||
assert_eq!(dict["twenty"], "there");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dict_push_skips_already_inserted_keys() {
|
||||
let mut dict = Dict::new();
|
||||
dict.insert(2, "2");
|
||||
dict.push("0");
|
||||
dict.insert(3, "3");
|
||||
dict.push("1");
|
||||
dict.push("4");
|
||||
assert_eq!(dict.len(), 5);
|
||||
assert_eq!(dict[0], "0");
|
||||
assert_eq!(dict[1], "1");
|
||||
assert_eq!(dict[2], "2");
|
||||
assert_eq!(dict[3], "3");
|
||||
assert_eq!(dict[4], "4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dict_push_remove_push_reuses_index() {
|
||||
let mut dict = Dict::new();
|
||||
dict.push("0");
|
||||
dict.push("1");
|
||||
dict.push("2");
|
||||
dict.remove(1);
|
||||
dict.push("a");
|
||||
dict.push("3");
|
||||
assert_eq!(dict.len(), 4);
|
||||
assert_eq!(dict[0], "0");
|
||||
assert_eq!(dict[1], "a");
|
||||
assert_eq!(dict[2], "2");
|
||||
assert_eq!(dict[3], "3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dict_first_and_last_are_correct() {
|
||||
let mut dict = Dict::new();
|
||||
assert_eq!(dict.first(), None);
|
||||
assert_eq!(dict.last(), None);
|
||||
dict.insert(4, "hi");
|
||||
dict.insert("string", "hi");
|
||||
assert_eq!(dict.first(), Some((4, &"hi")));
|
||||
assert_eq!(dict.last(), Some((4, &"hi")));
|
||||
dict.insert(2, "bye");
|
||||
assert_eq!(dict.first(), Some((2, &"bye")));
|
||||
assert_eq!(dict.last(), Some((4, &"hi")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dict_format_debug() {
|
||||
let mut dict = Dict::new();
|
||||
assert_eq!(format!("{:?}", dict), "()");
|
||||
assert_eq!(format!("{:#?}", dict), "()");
|
||||
|
||||
dict.insert(10, "hello");
|
||||
dict.insert("twenty", "there");
|
||||
dict.insert("sp ace", "quotes");
|
||||
assert_eq!(
|
||||
format!("{:?}", dict),
|
||||
r#"(10: "hello", "sp ace": "quotes", twenty: "there")"#,
|
||||
);
|
||||
assert_eq!(format!("{:#?}", dict).lines().collect::<Vec<_>>(), [
|
||||
"(",
|
||||
r#" 10: "hello","#,
|
||||
r#" "sp ace": "quotes","#,
|
||||
r#" twenty: "there","#,
|
||||
")",
|
||||
]);
|
||||
}
|
||||
}
|
@ -3,12 +3,10 @@
|
||||
#[macro_use]
|
||||
mod value;
|
||||
mod args;
|
||||
mod dict;
|
||||
mod scope;
|
||||
mod state;
|
||||
|
||||
pub use args::*;
|
||||
pub use dict::*;
|
||||
pub use scope::*;
|
||||
pub use state::*;
|
||||
pub use value::*;
|
||||
@ -451,7 +449,13 @@ impl Eval for Spanned<&Lit> {
|
||||
|
||||
fn eval(self, ctx: &mut EvalContext) -> Self::Output {
|
||||
match *self.v {
|
||||
Lit::Ident(ref v) => Value::Ident(v.clone()),
|
||||
Lit::Ident(ref v) => match ctx.state.scope.get(v.as_str()) {
|
||||
Some(value) => value.clone(),
|
||||
None => {
|
||||
ctx.diag(error!(self.span, "unknown variable"));
|
||||
Value::Error
|
||||
}
|
||||
},
|
||||
Lit::Bool(v) => Value::Bool(v),
|
||||
Lit::Int(v) => Value::Int(v),
|
||||
Lit::Float(v) => Value::Float(v),
|
||||
@ -459,28 +463,29 @@ impl Eval for Spanned<&Lit> {
|
||||
Lit::Percent(v) => Value::Relative(Relative::new(v / 100.0)),
|
||||
Lit::Color(v) => Value::Color(Color::Rgba(v)),
|
||||
Lit::Str(ref v) => Value::Str(v.clone()),
|
||||
Lit::Array(ref v) => Value::Array(v.with_span(self.span).eval(ctx)),
|
||||
Lit::Dict(ref v) => Value::Dict(v.with_span(self.span).eval(ctx)),
|
||||
Lit::Content(ref v) => Value::Content(v.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Eval for Spanned<&LitDict> {
|
||||
|
||||
impl Eval for Spanned<&Array> {
|
||||
type Output = ValueArray;
|
||||
|
||||
fn eval(self, ctx: &mut EvalContext) -> Self::Output {
|
||||
self.v.iter().map(|expr| expr.as_ref().eval(ctx)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for Spanned<&Dict> {
|
||||
type Output = ValueDict;
|
||||
|
||||
fn eval(self, ctx: &mut EvalContext) -> Self::Output {
|
||||
let mut dict = ValueDict::new();
|
||||
|
||||
for entry in &self.v.0 {
|
||||
let val = entry.expr.as_ref().eval(ctx);
|
||||
let spanned = val.with_span(entry.expr.span);
|
||||
if let Some(key) = &entry.key {
|
||||
dict.insert(&key.v, SpannedEntry::new(key.span, spanned));
|
||||
} else {
|
||||
dict.push(SpannedEntry::value(spanned));
|
||||
}
|
||||
}
|
||||
|
||||
dict
|
||||
self.v
|
||||
.iter()
|
||||
.map(|Named { name, expr }| (name.v.0.clone(), expr.as_ref().eval(ctx)))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@ -490,19 +495,27 @@ impl Eval for Spanned<&ExprCall> {
|
||||
fn eval(self, ctx: &mut EvalContext) -> Self::Output {
|
||||
let name = &self.v.name.v;
|
||||
let span = self.v.name.span;
|
||||
let dict = self.v.args.as_ref().eval(ctx);
|
||||
|
||||
if let Some(func) = ctx.state.scope.get(name) {
|
||||
let args = Args(dict.with_span(self.v.args.span));
|
||||
ctx.feedback.decos.push(Deco::Resolved.with_span(span));
|
||||
(func.clone())(args, ctx)
|
||||
} else {
|
||||
if !name.is_empty() {
|
||||
ctx.diag(error!(span, "unknown function"));
|
||||
ctx.feedback.decos.push(Deco::Unresolved.with_span(span));
|
||||
if let Some(value) = ctx.state.scope.get(name) {
|
||||
if let Value::Func(func) = value {
|
||||
let func = func.clone();
|
||||
ctx.feedback.decos.push(Deco::Resolved.with_span(span));
|
||||
|
||||
let mut args = self.v.args.as_ref().eval(ctx);
|
||||
let returned = func(ctx, &mut args);
|
||||
args.finish(ctx);
|
||||
|
||||
return returned;
|
||||
} else {
|
||||
let ty = value.type_name();
|
||||
ctx.diag(error!(span, "a value of type {} is not callable", ty));
|
||||
}
|
||||
Value::Dict(dict)
|
||||
} else if !name.is_empty() {
|
||||
ctx.diag(error!(span, "unknown function"));
|
||||
}
|
||||
|
||||
ctx.feedback.decos.push(Deco::Unresolved.with_span(span));
|
||||
Value::Error
|
||||
}
|
||||
}
|
||||
|
||||
@ -554,7 +567,7 @@ fn neg(ctx: &mut EvalContext, span: Span, value: Value) -> Value {
|
||||
Relative(v) => Relative(-v),
|
||||
Linear(v) => Linear(-v),
|
||||
v => {
|
||||
ctx.diag(error!(span, "cannot negate {}", v.ty()));
|
||||
ctx.diag(error!(span, "cannot negate {}", v.type_name()));
|
||||
Value::Error
|
||||
}
|
||||
}
|
||||
@ -589,7 +602,12 @@ fn add(ctx: &mut EvalContext, span: Span, lhs: Value, rhs: Value) -> Value {
|
||||
(Content(a), Content(b)) => Content(concat(a, b)),
|
||||
|
||||
(a, b) => {
|
||||
ctx.diag(error!(span, "cannot add {} and {}", a.ty(), b.ty()));
|
||||
ctx.diag(error!(
|
||||
span,
|
||||
"cannot add {} and {}",
|
||||
a.type_name(),
|
||||
b.type_name()
|
||||
));
|
||||
Value::Error
|
||||
}
|
||||
}
|
||||
@ -617,7 +635,12 @@ fn sub(ctx: &mut EvalContext, span: Span, lhs: Value, rhs: Value) -> Value {
|
||||
(Linear(a), Linear(b)) => Linear(a - b),
|
||||
|
||||
(a, b) => {
|
||||
ctx.diag(error!(span, "cannot subtract {1} from {0}", a.ty(), b.ty()));
|
||||
ctx.diag(error!(
|
||||
span,
|
||||
"cannot subtract {1} from {0}",
|
||||
a.type_name(),
|
||||
b.type_name()
|
||||
));
|
||||
Value::Error
|
||||
}
|
||||
}
|
||||
@ -652,7 +675,12 @@ fn mul(ctx: &mut EvalContext, span: Span, lhs: Value, rhs: Value) -> Value {
|
||||
(Str(a), Int(b)) => Str(a.repeat(0.max(b) as usize)),
|
||||
|
||||
(a, b) => {
|
||||
ctx.diag(error!(span, "cannot multiply {} with {}", a.ty(), b.ty()));
|
||||
ctx.diag(error!(
|
||||
span,
|
||||
"cannot multiply {} with {}",
|
||||
a.type_name(),
|
||||
b.type_name()
|
||||
));
|
||||
Value::Error
|
||||
}
|
||||
}
|
||||
@ -677,7 +705,12 @@ fn div(ctx: &mut EvalContext, span: Span, lhs: Value, rhs: Value) -> Value {
|
||||
(Linear(a), Float(b)) => Linear(a / b),
|
||||
|
||||
(a, b) => {
|
||||
ctx.diag(error!(span, "cannot divide {} by {}", a.ty(), b.ty()));
|
||||
ctx.diag(error!(
|
||||
span,
|
||||
"cannot divide {} by {}",
|
||||
a.type_name(),
|
||||
b.type_name()
|
||||
));
|
||||
Value::Error
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,12 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
|
||||
use super::value::ValueFunc;
|
||||
use super::Value;
|
||||
|
||||
/// A map from identifiers to functions.
|
||||
#[derive(Default, Clone, PartialEq)]
|
||||
pub struct Scope {
|
||||
functions: HashMap<String, ValueFunc>,
|
||||
values: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
@ -18,19 +18,19 @@ impl Scope {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Return the function with the given name if there is one.
|
||||
pub fn get(&self, name: &str) -> Option<&ValueFunc> {
|
||||
self.functions.get(name)
|
||||
/// Return the value of the given variable.
|
||||
pub fn get(&self, var: &str) -> Option<&Value> {
|
||||
self.values.get(var)
|
||||
}
|
||||
|
||||
/// Associate the given name with the function.
|
||||
pub fn set(&mut self, name: impl Into<String>, function: ValueFunc) {
|
||||
self.functions.insert(name.into(), function);
|
||||
/// Store the value for the given variable.
|
||||
pub fn set(&mut self, var: impl Into<String>, value: impl Into<Value>) {
|
||||
self.values.insert(var.into(), value.into());
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Scope {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.debug_set().entries(self.functions.keys()).finish()
|
||||
self.values.fmt(f)
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,21 @@
|
||||
//! Computational values.
|
||||
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
|
||||
use fontdock::{FontStretch, FontStyle, FontWeight};
|
||||
|
||||
use super::{Args, Dict, Eval, EvalContext, SpannedEntry};
|
||||
use super::{Args, Eval, EvalContext};
|
||||
use crate::color::Color;
|
||||
use crate::diag::Diag;
|
||||
use crate::geom::{Dir, Length, Linear, Relative};
|
||||
use crate::paper::Paper;
|
||||
use crate::syntax::{Ident, Spanned, SynTree, WithSpan};
|
||||
use crate::geom::{Length, Linear, Relative};
|
||||
use crate::syntax::{Spanned, SynTree, WithSpan};
|
||||
|
||||
/// A computational value.
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum Value {
|
||||
/// The value that indicates the absence of a meaningful value.
|
||||
None,
|
||||
/// An identifier: `ident`.
|
||||
Ident(Ident),
|
||||
/// A boolean: `true, false`.
|
||||
Bool(bool),
|
||||
/// An integer: `120`.
|
||||
@ -36,34 +32,46 @@ pub enum Value {
|
||||
Color(Color),
|
||||
/// A string: `"string"`.
|
||||
Str(String),
|
||||
/// A dictionary value: `(false, 12cm, greeting: "hi")`.
|
||||
/// An array value: `(1, "hi", 12cm)`.
|
||||
Array(ValueArray),
|
||||
/// A dictionary value: `(color: #f79143, pattern: dashed)`.
|
||||
Dict(ValueDict),
|
||||
/// A content value: `{*Hi* there}`.
|
||||
Content(SynTree),
|
||||
Content(ValueContent),
|
||||
/// An executable function.
|
||||
Func(ValueFunc),
|
||||
/// Any object.
|
||||
Any(ValueAny),
|
||||
/// The result of invalid operations.
|
||||
Error,
|
||||
}
|
||||
|
||||
impl Value {
|
||||
/// The natural-language name of this value's type for use in error
|
||||
/// messages.
|
||||
pub fn ty(&self) -> &'static str {
|
||||
/// Try to cast the value into a specific type.
|
||||
pub fn cast<T>(self) -> CastResult<T, Self>
|
||||
where
|
||||
T: Cast<Value>,
|
||||
{
|
||||
T::cast(self)
|
||||
}
|
||||
|
||||
/// The name of the stored value's type.
|
||||
pub fn type_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::None => "none",
|
||||
Self::Ident(_) => "identifier",
|
||||
Self::Bool(_) => "bool",
|
||||
Self::Int(_) => "integer",
|
||||
Self::Float(_) => "float",
|
||||
Self::Relative(_) => "relative",
|
||||
Self::Length(_) => "length",
|
||||
Self::Linear(_) => "linear",
|
||||
Self::Color(_) => "color",
|
||||
Self::Str(_) => "string",
|
||||
Self::Dict(_) => "dict",
|
||||
Self::Content(_) => "content",
|
||||
Self::Func(_) => "function",
|
||||
Self::Bool(_) => bool::TYPE_NAME,
|
||||
Self::Int(_) => i64::TYPE_NAME,
|
||||
Self::Float(_) => f64::TYPE_NAME,
|
||||
Self::Relative(_) => Relative::TYPE_NAME,
|
||||
Self::Length(_) => Length::TYPE_NAME,
|
||||
Self::Linear(_) => Linear::TYPE_NAME,
|
||||
Self::Color(_) => Color::TYPE_NAME,
|
||||
Self::Str(_) => String::TYPE_NAME,
|
||||
Self::Array(_) => ValueArray::TYPE_NAME,
|
||||
Self::Dict(_) => ValueDict::TYPE_NAME,
|
||||
Self::Content(_) => ValueContent::TYPE_NAME,
|
||||
Self::Func(_) => ValueFunc::TYPE_NAME,
|
||||
Self::Any(v) => v.type_name(),
|
||||
Self::Error => "error",
|
||||
}
|
||||
}
|
||||
@ -81,13 +89,6 @@ impl Eval for &Value {
|
||||
// Pass through.
|
||||
Value::Content(tree) => tree.eval(ctx),
|
||||
|
||||
// Forward to each dictionary entry.
|
||||
Value::Dict(dict) => {
|
||||
for entry in dict.values() {
|
||||
entry.value.v.eval(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
// Format with debug.
|
||||
val => ctx.push(ctx.make_text_node(format!("{:?}", val))),
|
||||
}
|
||||
@ -104,7 +105,6 @@ impl Debug for Value {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::None => f.pad("none"),
|
||||
Self::Ident(v) => v.fmt(f),
|
||||
Self::Bool(v) => v.fmt(f),
|
||||
Self::Int(v) => v.fmt(f),
|
||||
Self::Float(v) => v.fmt(f),
|
||||
@ -113,45 +113,36 @@ impl Debug for Value {
|
||||
Self::Linear(v) => v.fmt(f),
|
||||
Self::Color(v) => v.fmt(f),
|
||||
Self::Str(v) => v.fmt(f),
|
||||
Self::Array(v) => v.fmt(f),
|
||||
Self::Dict(v) => v.fmt(f),
|
||||
Self::Content(v) => v.fmt(f),
|
||||
Self::Func(v) => v.fmt(f),
|
||||
Self::Any(v) => v.fmt(f),
|
||||
Self::Error => f.pad("<error>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A dictionary of values.
|
||||
///
|
||||
/// # Example
|
||||
/// ```typst
|
||||
/// (false, 12cm, greeting: "hi")
|
||||
/// ```
|
||||
pub type ValueDict = Dict<SpannedEntry<Value>>;
|
||||
/// An array value: `(1, "hi", 12cm)`.
|
||||
pub type ValueArray = Vec<Value>;
|
||||
|
||||
/// An wrapper around a reference-counted function trait object.
|
||||
///
|
||||
/// The dynamic function object is wrapped in an `Rc` to keep [`Value`]
|
||||
/// cloneable.
|
||||
///
|
||||
/// _Note_: This is needed because the compiler can't `derive(PartialEq)` for
|
||||
/// [`Value`] when directly putting the `Rc` in there, see the [Rust
|
||||
/// Issue].
|
||||
///
|
||||
/// [Rust Issue]: https://github.com/rust-lang/rust/issues/31740
|
||||
/// A dictionary value: `(color: #f79143, pattern: dashed)`.
|
||||
pub type ValueDict = HashMap<String, Value>;
|
||||
|
||||
/// A content value: `{*Hi* there}`.
|
||||
pub type ValueContent = SynTree;
|
||||
|
||||
/// A wrapper around a reference-counted executable function.
|
||||
#[derive(Clone)]
|
||||
pub struct ValueFunc(pub Rc<Func>);
|
||||
|
||||
/// The signature of executable functions.
|
||||
type Func = dyn Fn(Args, &mut EvalContext) -> Value;
|
||||
pub struct ValueFunc(Rc<dyn Fn(&mut EvalContext, &mut Args) -> Value>);
|
||||
|
||||
impl ValueFunc {
|
||||
/// Create a new function value from a rust function or closure.
|
||||
pub fn new<F>(f: F) -> Self
|
||||
pub fn new<F>(func: F) -> Self
|
||||
where
|
||||
F: Fn(Args, &mut EvalContext) -> Value + 'static,
|
||||
F: Fn(&mut EvalContext, &mut Args) -> Value + 'static,
|
||||
{
|
||||
Self(Rc::new(f))
|
||||
Self(Rc::new(func))
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,7 +153,7 @@ impl PartialEq for ValueFunc {
|
||||
}
|
||||
|
||||
impl Deref for ValueFunc {
|
||||
type Target = Func;
|
||||
type Target = dyn Fn(&mut EvalContext, &mut Args) -> Value;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.0.as_ref()
|
||||
@ -175,160 +166,288 @@ impl Debug for ValueFunc {
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to convert a value into a more specific type.
|
||||
pub trait TryFromValue: Sized {
|
||||
/// Try to convert the value into yourself.
|
||||
fn try_from_value(value: Spanned<Value>) -> Conv<Self>;
|
||||
/// A wrapper around a dynamic value.
|
||||
pub struct ValueAny(Box<dyn Bounds>);
|
||||
|
||||
impl ValueAny {
|
||||
/// Create a new instance from any value that satisifies the required bounds.
|
||||
pub fn new<T>(any: T) -> Self
|
||||
where
|
||||
T: Type + Debug + Clone + PartialEq + 'static,
|
||||
{
|
||||
Self(Box::new(any))
|
||||
}
|
||||
|
||||
/// Whether the wrapped type is `T`.
|
||||
pub fn is<T: 'static>(&self) -> bool {
|
||||
self.0.as_any().is::<T>()
|
||||
}
|
||||
|
||||
/// Try to downcast to a specific type.
|
||||
pub fn downcast<T: 'static>(self) -> Result<T, Self> {
|
||||
if self.is::<T>() {
|
||||
Ok(*self.0.into_any().downcast().unwrap())
|
||||
} else {
|
||||
Err(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to downcast to a reference to a specific type.
|
||||
pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
|
||||
self.0.as_any().downcast_ref()
|
||||
}
|
||||
|
||||
/// The name of the stored object's type.
|
||||
pub fn type_name(&self) -> &'static str {
|
||||
self.0.dyn_type_name()
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of a conversion.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Conv<T> {
|
||||
/// Success conversion.
|
||||
impl Clone for ValueAny {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.dyn_clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ValueAny {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.dyn_eq(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for ValueAny {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
trait Bounds: Debug + 'static {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn into_any(self: Box<Self>) -> Box<dyn Any>;
|
||||
fn dyn_eq(&self, other: &ValueAny) -> bool;
|
||||
fn dyn_clone(&self) -> Box<dyn Bounds>;
|
||||
fn dyn_type_name(&self) -> &'static str;
|
||||
}
|
||||
|
||||
impl<T> Bounds for T
|
||||
where
|
||||
T: Type + Debug + Clone + PartialEq + 'static,
|
||||
{
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn into_any(self: Box<Self>) -> Box<dyn Any> {
|
||||
self
|
||||
}
|
||||
|
||||
fn dyn_eq(&self, other: &ValueAny) -> bool {
|
||||
if let Some(other) = other.downcast_ref::<Self>() {
|
||||
self == other
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn dyn_clone(&self) -> Box<dyn Bounds> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn dyn_type_name(&self) -> &'static str {
|
||||
T::TYPE_NAME
|
||||
}
|
||||
}
|
||||
|
||||
/// Types that can be stored in values.
|
||||
pub trait Type {
|
||||
/// The name of the type.
|
||||
const TYPE_NAME: &'static str;
|
||||
}
|
||||
|
||||
impl<T> Type for Spanned<T>
|
||||
where
|
||||
T: Type,
|
||||
{
|
||||
const TYPE_NAME: &'static str = T::TYPE_NAME;
|
||||
}
|
||||
|
||||
/// Cast from a value to a specific type.
|
||||
pub trait Cast<V>: Type + Sized {
|
||||
/// Try to cast the value into an instance of `Self`.
|
||||
fn cast(value: V) -> CastResult<Self, V>;
|
||||
}
|
||||
|
||||
/// The result of casting a value to a specific type.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum CastResult<T, V> {
|
||||
/// The value was cast successfully.
|
||||
Ok(T),
|
||||
/// Sucessful conversion with a warning.
|
||||
Warn(T, Diag),
|
||||
/// Unsucessful conversion, gives back the value alongside the error.
|
||||
Err(Value, Diag),
|
||||
/// The value was cast successfully, but with a warning message.
|
||||
Warn(T, String),
|
||||
/// The value could not be cast into the specified type.
|
||||
Err(V),
|
||||
}
|
||||
|
||||
impl<T> Conv<T> {
|
||||
/// Map the conversion result.
|
||||
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Conv<U> {
|
||||
impl<T, V> CastResult<T, V> {
|
||||
/// Access the conversion resulting, discarding a possibly existing warning.
|
||||
pub fn ok(self) -> Option<T> {
|
||||
match self {
|
||||
Conv::Ok(t) => Conv::Ok(f(t)),
|
||||
Conv::Warn(t, warn) => Conv::Warn(f(t), warn),
|
||||
Conv::Err(v, err) => Conv::Err(v, err),
|
||||
CastResult::Ok(t) | CastResult::Warn(t, _) => Some(t),
|
||||
CastResult::Err(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: TryFromValue> TryFromValue for Spanned<T> {
|
||||
fn try_from_value(value: Spanned<Value>) -> Conv<Self> {
|
||||
impl<T> Cast<Spanned<Value>> for T
|
||||
where
|
||||
T: Cast<Value>,
|
||||
{
|
||||
fn cast(value: Spanned<Value>) -> CastResult<Self, Spanned<Value>> {
|
||||
let span = value.span;
|
||||
T::try_from_value(value).map(|v| v.with_span(span))
|
||||
match T::cast(value.v) {
|
||||
CastResult::Ok(t) => CastResult::Ok(t),
|
||||
CastResult::Warn(t, m) => CastResult::Warn(t, m),
|
||||
CastResult::Err(v) => CastResult::Err(v.with_span(span)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A value type that matches [identifier](Value::Ident) and [string](Value::Str) values.
|
||||
pub struct StringLike(pub String);
|
||||
|
||||
impl From<StringLike> for String {
|
||||
fn from(like: StringLike) -> String {
|
||||
like.0
|
||||
impl<T> Cast<Spanned<Value>> for Spanned<T>
|
||||
where
|
||||
T: Cast<Value>,
|
||||
{
|
||||
fn cast(value: Spanned<Value>) -> CastResult<Self, Spanned<Value>> {
|
||||
let span = value.span;
|
||||
match T::cast(value.v) {
|
||||
CastResult::Ok(t) => CastResult::Ok(t.with_span(span)),
|
||||
CastResult::Warn(t, m) => CastResult::Warn(t.with_span(span), m),
|
||||
CastResult::Err(v) => CastResult::Err(v.with_span(span)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for StringLike {
|
||||
type Target = str;
|
||||
macro_rules! impl_primitive {
|
||||
($type:ty:
|
||||
$type_name:literal,
|
||||
$variant:path
|
||||
$(, $pattern:pat => $out:expr)* $(,)?
|
||||
) => {
|
||||
impl Type for $type {
|
||||
const TYPE_NAME: &'static str = $type_name;
|
||||
}
|
||||
|
||||
fn deref(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
impl From<$type> for Value {
|
||||
fn from(v: $type) -> Self {
|
||||
$variant(v)
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement [`TryFromValue`] through a match.
|
||||
macro_rules! try_from_match {
|
||||
($type:ty[$name:literal] $(@ $span:ident)?: $($pattern:pat => $output:expr),* $(,)?) => {
|
||||
impl $crate::eval::TryFromValue for $type {
|
||||
fn try_from_value(value: Spanned<Value>) -> $crate::eval::Conv<Self> {
|
||||
use $crate::eval::Conv;
|
||||
#[allow(unused)]
|
||||
$(let $span = value.span;)?
|
||||
#[allow(unreachable_patterns)]
|
||||
match value.v {
|
||||
$($pattern => Conv::Ok($output)),*,
|
||||
v => {
|
||||
let e = error!("expected {}, found {}", $name, v.ty());
|
||||
Conv::Err(v, e)
|
||||
}
|
||||
impl Cast<Value> for $type {
|
||||
fn cast(value: Value) -> CastResult<Self, Value> {
|
||||
match value {
|
||||
$variant(v) => CastResult::Ok(v),
|
||||
$($pattern => CastResult::Ok($out),)*
|
||||
v => CastResult::Err(v),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Implement [`TryFromValue`] through a function parsing an identifier.
|
||||
macro_rules! try_from_id {
|
||||
($type:ty[$name:literal]: $from_str:expr) => {
|
||||
impl $crate::eval::TryFromValue for $type {
|
||||
fn try_from_value(value: Spanned<Value>) -> $crate::eval::Conv<Self> {
|
||||
use $crate::eval::Conv;
|
||||
let v = value.v;
|
||||
if let Value::Ident(id) = v {
|
||||
if let Some(v) = $from_str(&id) {
|
||||
Conv::Ok(v)
|
||||
} else {
|
||||
Conv::Err(Value::Ident(id), error!("invalid {}", $name))
|
||||
}
|
||||
} else {
|
||||
let e = error!("expected identifier, found {}", v.ty());
|
||||
Conv::Err(v, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
impl_primitive! { bool: "boolean", Value::Bool }
|
||||
impl_primitive! { i64: "integer", Value::Int }
|
||||
impl_primitive! { Length: "length", Value::Length }
|
||||
impl_primitive! { Relative: "relative", Value::Relative }
|
||||
impl_primitive! { Color: "color", Value::Color }
|
||||
impl_primitive! { String: "string", Value::Str }
|
||||
impl_primitive! { ValueArray: "array", Value::Array }
|
||||
impl_primitive! { ValueDict: "dictionary", Value::Dict }
|
||||
impl_primitive! { ValueContent: "content", Value::Content }
|
||||
impl_primitive! { ValueFunc: "function", Value::Func }
|
||||
|
||||
try_from_match!(Value["value"]: v => v);
|
||||
try_from_match!(Ident["identifier"]: Value::Ident(v) => v);
|
||||
try_from_match!(bool["bool"]: Value::Bool(v) => v);
|
||||
try_from_match!(i64["integer"]: Value::Int(v) => v);
|
||||
try_from_match!(f64["float"]:
|
||||
impl_primitive! {
|
||||
f64: "float",
|
||||
Value::Float,
|
||||
Value::Int(v) => v as f64,
|
||||
Value::Float(v) => v,
|
||||
);
|
||||
try_from_match!(Length["length"]: Value::Length(v) => v);
|
||||
try_from_match!(Relative["relative"]: Value::Relative(v) => v);
|
||||
try_from_match!(Linear["linear"]:
|
||||
Value::Linear(v) => v,
|
||||
}
|
||||
|
||||
impl_primitive! {
|
||||
Linear: "linear",
|
||||
Value::Linear,
|
||||
Value::Length(v) => v.into(),
|
||||
Value::Relative(v) => v.into(),
|
||||
);
|
||||
try_from_match!(Color["color"]: Value::Color(v) => v);
|
||||
try_from_match!(String["string"]: Value::Str(v) => v);
|
||||
try_from_match!(SynTree["tree"]: Value::Content(v) => v);
|
||||
try_from_match!(ValueDict["dictionary"]: Value::Dict(v) => v);
|
||||
try_from_match!(ValueFunc["function"]: Value::Func(v) => v);
|
||||
try_from_match!(StringLike["identifier or string"]:
|
||||
Value::Ident(Ident(v)) => Self(v),
|
||||
Value::Str(v) => Self(v),
|
||||
);
|
||||
try_from_id!(Dir["direction"]: |v| match v {
|
||||
"ltr" | "left-to-right" => Some(Self::LTR),
|
||||
"rtl" | "right-to-left" => Some(Self::RTL),
|
||||
"ttb" | "top-to-bottom" => Some(Self::TTB),
|
||||
"btt" | "bottom-to-top" => Some(Self::BTT),
|
||||
_ => None,
|
||||
});
|
||||
try_from_id!(FontStyle["font style"]: Self::from_str);
|
||||
try_from_id!(FontStretch["font stretch"]: Self::from_str);
|
||||
try_from_id!(Paper["paper"]: Self::from_name);
|
||||
}
|
||||
|
||||
impl TryFromValue for FontWeight {
|
||||
fn try_from_value(value: Spanned<Value>) -> Conv<Self> {
|
||||
match value.v {
|
||||
Value::Int(number) => {
|
||||
let [min, max] = [Self::THIN, Self::BLACK];
|
||||
if number < i64::from(min.to_number()) {
|
||||
Conv::Warn(min, warning!("the minimum font weight is {:#?}", min))
|
||||
} else if number > i64::from(max.to_number()) {
|
||||
Conv::Warn(max, warning!("the maximum font weight is {:#?}", max))
|
||||
} else {
|
||||
Conv::Ok(Self::from_number(number as u16))
|
||||
}
|
||||
}
|
||||
Value::Ident(id) => {
|
||||
if let Some(weight) = Self::from_str(&id) {
|
||||
Conv::Ok(weight)
|
||||
} else {
|
||||
Conv::Err(Value::Ident(id), error!("invalid font weight"))
|
||||
}
|
||||
}
|
||||
v => {
|
||||
let e = error!("expected font weight, found {}", v.ty());
|
||||
Conv::Err(v, e)
|
||||
}
|
||||
}
|
||||
impl From<&str> for Value {
|
||||
fn from(v: &str) -> Self {
|
||||
Self::Str(v.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> From<F> for Value
|
||||
where
|
||||
F: Fn(&mut EvalContext, &mut Args) -> Value + 'static,
|
||||
{
|
||||
fn from(func: F) -> Self {
|
||||
Self::Func(ValueFunc::new(func))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ValueAny> for Value {
|
||||
fn from(v: ValueAny) -> Self {
|
||||
Self::Any(v)
|
||||
}
|
||||
}
|
||||
|
||||
/// Make a type usable with [`ValueAny`].
|
||||
///
|
||||
/// Given a type `T`, this implements the following traits:
|
||||
/// - [`Type`] for `T`,
|
||||
/// - [`From<T>`](From) for [`Value`],
|
||||
/// - [`Cast<Value>`](Cast) for `T`.
|
||||
#[macro_export]
|
||||
macro_rules! impl_type {
|
||||
($type:ty:
|
||||
$type_name:literal
|
||||
$(, $pattern:pat => $out:expr)*
|
||||
$(, #($anyvar:ident: $anytype:ty) => $anyout:expr)*
|
||||
$(,)?
|
||||
) => {
|
||||
impl $crate::eval::Type for $type {
|
||||
const TYPE_NAME: &'static str = $type_name;
|
||||
}
|
||||
|
||||
impl From<$type> for $crate::eval::Value {
|
||||
fn from(any: $type) -> Self {
|
||||
$crate::eval::Value::Any($crate::eval::ValueAny::new(any))
|
||||
}
|
||||
}
|
||||
|
||||
impl $crate::eval::Cast<$crate::eval::Value> for $type {
|
||||
fn cast(
|
||||
value: $crate::eval::Value,
|
||||
) -> $crate::eval::CastResult<Self, $crate::eval::Value> {
|
||||
use $crate::eval::*;
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
match value {
|
||||
$($pattern => CastResult::Ok($out),)*
|
||||
Value::Any(mut any) => {
|
||||
any = match any.downcast::<Self>() {
|
||||
Ok(t) => return CastResult::Ok(t),
|
||||
Err(any) => any,
|
||||
};
|
||||
|
||||
$(any = match any.downcast::<$anytype>() {
|
||||
Ok($anyvar) => return CastResult::Ok($anyout),
|
||||
Err(any) => any,
|
||||
};)*
|
||||
|
||||
CastResult::Err(Value::Any(any))
|
||||
},
|
||||
v => CastResult::Err(v),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -208,14 +208,14 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_length_formats_correctly() {
|
||||
fn test_length_unit_conversion() {
|
||||
assert!((Length::mm(150.0).to_cm() - 15.0) < 1e-4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_length_formatting() {
|
||||
assert_eq!(Length::pt(-28.34).to_string(), "-1.00cm".to_string());
|
||||
assert_eq!(Length::pt(23.0).to_string(), "23.00pt".to_string());
|
||||
assert_eq!(Length::cm(12.728).to_string(), "12.73cm".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_length_unit_conversion() {
|
||||
assert!((Length::mm(150.0).to_cm() - 15.0) < 1e-4);
|
||||
}
|
||||
}
|
||||
|
@ -6,14 +6,14 @@ use crate::prelude::*;
|
||||
|
||||
/// `image`: Insert an image.
|
||||
///
|
||||
/// # Positional arguments
|
||||
/// - Path (`string`): The path to the image file.
|
||||
///
|
||||
/// Supports PNG and JPEG files.
|
||||
pub fn image(mut args: Args, ctx: &mut EvalContext) -> Value {
|
||||
let path = args.need::<_, Spanned<String>>(ctx, 0, "path");
|
||||
let width = args.get::<_, Linear>(ctx, "width");
|
||||
let height = args.get::<_, Linear>(ctx, "height");
|
||||
///
|
||||
/// # Positional arguments
|
||||
/// - Path to image file: of type `string`.
|
||||
pub fn image(ctx: &mut EvalContext, args: &mut Args) -> Value {
|
||||
let path = args.require::<Spanned<String>>(ctx, "path to image file");
|
||||
let width = args.get(ctx, "width");
|
||||
let height = args.get(ctx, "height");
|
||||
|
||||
if let Some(path) = path {
|
||||
let mut env = ctx.env.borrow_mut();
|
||||
|
@ -1,51 +1,44 @@
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
use crate::eval::Softness;
|
||||
use crate::geom::{Length, Linear};
|
||||
use crate::layout::{Expansion, Fixed, Spacing, Stack};
|
||||
use crate::paper::{Paper, PaperClass};
|
||||
use crate::prelude::*;
|
||||
|
||||
/// `align`: Align content along the layouting axes.
|
||||
///
|
||||
/// Which axis an alignment should apply to (main or cross) is inferred from
|
||||
/// either the argument itself (for anything other than `center`) or from the
|
||||
/// second argument if present, defaulting to the cross axis for a single
|
||||
/// `center` alignment.
|
||||
///
|
||||
/// # Positional arguments
|
||||
/// - first (optional, `Alignment`): An alignment for any of the two axes.
|
||||
/// - second (optional, `Alignment`): An alignment for the other axis.
|
||||
/// - Alignments: variadic, of type `alignment`.
|
||||
///
|
||||
/// # Named arguments
|
||||
/// - `horizontal` (`Alignment`): An alignment for the horizontal axis.
|
||||
/// - `vertical` (`Alignment`): An alignment for the vertical axis.
|
||||
/// - Horizontal alignment: `horizontal`, of type `alignment`.
|
||||
/// - Vertical alignment: `vertical`, of type `alignment`.
|
||||
///
|
||||
/// # Enumerations
|
||||
/// - `Alignment`
|
||||
/// # Relevant types and constants
|
||||
/// - Type `alignment`
|
||||
/// - `left`
|
||||
/// - `right`
|
||||
/// - `top`
|
||||
/// - `bottom`
|
||||
/// - `center`
|
||||
///
|
||||
/// # Notes
|
||||
/// Which axis an alignment should apply to (main or cross) is inferred from
|
||||
/// either the argument itself (for anything other than `center`) or from the
|
||||
/// second argument if present, defaulting to the cross axis for a single
|
||||
/// `center` alignment.
|
||||
pub fn align(mut args: Args, ctx: &mut EvalContext) -> Value {
|
||||
pub fn align(ctx: &mut EvalContext, args: &mut Args) -> Value {
|
||||
let snapshot = ctx.state.clone();
|
||||
let body = args.find::<SynTree>();
|
||||
let first = args.get::<_, Spanned<SpecAlign>>(ctx, 0);
|
||||
let second = args.get::<_, Spanned<SpecAlign>>(ctx, 1);
|
||||
let hor = args.get::<_, Spanned<SpecAlign>>(ctx, "horizontal");
|
||||
let ver = args.get::<_, Spanned<SpecAlign>>(ctx, "vertical");
|
||||
args.done(ctx);
|
||||
|
||||
let prev_main = ctx.state.align.main;
|
||||
let first = args.find(ctx);
|
||||
let second = args.find(ctx);
|
||||
let hor = args.get(ctx, "horizontal");
|
||||
let ver = args.get(ctx, "vertical");
|
||||
|
||||
let mut had = Gen::uniform(false);
|
||||
let mut had_center = false;
|
||||
|
||||
for (axis, Spanned { v: arg, span }) in first
|
||||
.into_iter()
|
||||
.chain(second.into_iter())
|
||||
.map(|arg| (arg.v.axis(), arg))
|
||||
.map(|arg: Spanned<Alignment>| (arg.v.axis(), arg))
|
||||
.chain(hor.into_iter().map(|arg| (Some(SpecAxis::Horizontal), arg)))
|
||||
.chain(ver.into_iter().map(|arg| (Some(SpecAxis::Vertical), arg)))
|
||||
{
|
||||
@ -56,10 +49,7 @@ pub fn align(mut args: Args, ctx: &mut EvalContext) -> Value {
|
||||
let gen_align = arg.switch(ctx.state.flow);
|
||||
|
||||
if arg.axis().map_or(false, |a| a != axis) {
|
||||
ctx.diag(error!(
|
||||
span,
|
||||
"invalid alignment `{}` for {} axis", arg, axis,
|
||||
));
|
||||
ctx.diag(error!(span, "invalid alignment for {} axis", axis));
|
||||
} else if had.get(gen_axis) {
|
||||
ctx.diag(error!(span, "duplicate alignment for {} axis", axis));
|
||||
} else {
|
||||
@ -69,7 +59,7 @@ pub fn align(mut args: Args, ctx: &mut EvalContext) -> Value {
|
||||
} else {
|
||||
// We don't know the axis: This has to be a `center` alignment for a
|
||||
// positional argument.
|
||||
debug_assert_eq!(arg, SpecAlign::Center);
|
||||
debug_assert_eq!(arg, Alignment::Center);
|
||||
|
||||
if had.main && had.cross {
|
||||
ctx.diag(error!(span, "duplicate alignment"));
|
||||
@ -104,12 +94,12 @@ pub fn align(mut args: Args, ctx: &mut EvalContext) -> Value {
|
||||
ctx.state.align.cross = Align::Center;
|
||||
}
|
||||
|
||||
if ctx.state.align.main != prev_main {
|
||||
if ctx.state.align.main != snapshot.align.main {
|
||||
ctx.end_par_group();
|
||||
ctx.start_par_group();
|
||||
}
|
||||
|
||||
if let Some(body) = body {
|
||||
if let Some(body) = args.find::<ValueContent>(ctx) {
|
||||
body.eval(ctx);
|
||||
ctx.state = snapshot;
|
||||
}
|
||||
@ -117,30 +107,18 @@ pub fn align(mut args: Args, ctx: &mut EvalContext) -> Value {
|
||||
Value::None
|
||||
}
|
||||
|
||||
/// An argument to `[align]`.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
||||
enum SpecAlign {
|
||||
pub(crate) enum Alignment {
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
Top,
|
||||
Bottom,
|
||||
Center,
|
||||
}
|
||||
|
||||
try_from_id!(SpecAlign["alignment"]: |v| match v {
|
||||
"left" => Some(Self::Left),
|
||||
"right" => Some(Self::Right),
|
||||
"top" => Some(Self::Top),
|
||||
"bottom" => Some(Self::Bottom),
|
||||
"center" => Some(Self::Center),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
impl SpecAlign {
|
||||
impl Alignment {
|
||||
/// The specific axis this alignment refers to.
|
||||
///
|
||||
/// Returns `None` if this is `Center` since the axis is unknown.
|
||||
pub fn axis(self) -> Option<SpecAxis> {
|
||||
fn axis(self) -> Option<SpecAxis> {
|
||||
match self {
|
||||
Self::Left => Some(SpecAxis::Horizontal),
|
||||
Self::Right => Some(SpecAxis::Horizontal),
|
||||
@ -151,7 +129,7 @@ impl SpecAlign {
|
||||
}
|
||||
}
|
||||
|
||||
impl Switch for SpecAlign {
|
||||
impl Switch for Alignment {
|
||||
type Other = Align;
|
||||
|
||||
fn switch(self, flow: Flow) -> Self::Other {
|
||||
@ -174,38 +152,34 @@ impl Switch for SpecAlign {
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SpecAlign {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.pad(match self {
|
||||
Self::Left => "left",
|
||||
Self::Right => "right",
|
||||
Self::Top => "top",
|
||||
Self::Bottom => "bottom",
|
||||
Self::Center => "center",
|
||||
})
|
||||
}
|
||||
impl_type! {
|
||||
Alignment: "alignment"
|
||||
}
|
||||
|
||||
/// `box`: Layout content into a box.
|
||||
///
|
||||
/// # Named arguments
|
||||
/// - `width` (`linear` relative to parent width): The width of the box.
|
||||
/// - `height` (`linear` relative to parent height): The height of the box.
|
||||
pub fn boxed(mut args: Args, ctx: &mut EvalContext) -> Value {
|
||||
/// - Width of the box: `width`, of type `linear` relative to parent width.
|
||||
/// - Height of the box: `height`, of type `linear` relative to parent height.
|
||||
pub fn boxed(ctx: &mut EvalContext, args: &mut Args) -> Value {
|
||||
let snapshot = ctx.state.clone();
|
||||
let body = args.find::<SynTree>().unwrap_or_default();
|
||||
let width = args.get::<_, Linear>(ctx, "width");
|
||||
let height = args.get::<_, Linear>(ctx, "height");
|
||||
let main = args.get::<_, Spanned<Dir>>(ctx, "main-dir");
|
||||
let cross = args.get::<_, Spanned<Dir>>(ctx, "cross-dir");
|
||||
|
||||
let width = args.get(ctx, "width");
|
||||
let height = args.get(ctx, "height");
|
||||
let main = args.get(ctx, "main-dir");
|
||||
let cross = args.get(ctx, "cross-dir");
|
||||
|
||||
ctx.set_flow(Gen::new(main, cross));
|
||||
args.done(ctx);
|
||||
|
||||
let flow = ctx.state.flow;
|
||||
let align = ctx.state.align;
|
||||
|
||||
ctx.start_content_group();
|
||||
body.eval(ctx);
|
||||
|
||||
if let Some(body) = args.find::<ValueContent>(ctx) {
|
||||
body.eval(ctx);
|
||||
}
|
||||
|
||||
let children = ctx.end_content_group();
|
||||
|
||||
ctx.push(Fixed {
|
||||
@ -227,31 +201,34 @@ pub fn boxed(mut args: Args, ctx: &mut EvalContext) -> Value {
|
||||
Value::None
|
||||
}
|
||||
|
||||
impl_type! {
|
||||
Dir: "direction"
|
||||
}
|
||||
|
||||
/// `h`: Add horizontal spacing.
|
||||
///
|
||||
/// # Positional arguments
|
||||
/// - Spacing (`linear` relative to font size): The amount of spacing.
|
||||
pub fn h(args: Args, ctx: &mut EvalContext) -> Value {
|
||||
spacing(args, ctx, SpecAxis::Horizontal)
|
||||
/// - Amount of spacing: of type `linear` relative to current font size.
|
||||
pub fn h(ctx: &mut EvalContext, args: &mut Args) -> Value {
|
||||
spacing(ctx, args, SpecAxis::Horizontal)
|
||||
}
|
||||
|
||||
/// `v`: Add vertical spacing.
|
||||
///
|
||||
/// # Positional arguments
|
||||
/// - Spacing (`linear` relative to font size): The amount of spacing.
|
||||
pub fn v(args: Args, ctx: &mut EvalContext) -> Value {
|
||||
spacing(args, ctx, SpecAxis::Vertical)
|
||||
/// - Amount of spacing: of type `linear` relative to current font size.
|
||||
pub fn v(ctx: &mut EvalContext, args: &mut Args) -> Value {
|
||||
spacing(ctx, args, SpecAxis::Vertical)
|
||||
}
|
||||
|
||||
/// Apply spacing along a specific axis.
|
||||
fn spacing(mut args: Args, ctx: &mut EvalContext, axis: SpecAxis) -> Value {
|
||||
let spacing = args.need::<_, Linear>(ctx, 0, "spacing");
|
||||
args.done(ctx);
|
||||
fn spacing(ctx: &mut EvalContext, args: &mut Args, axis: SpecAxis) -> Value {
|
||||
let spacing: Option<Linear> = args.require(ctx, "spacing");
|
||||
|
||||
if let Some(linear) = spacing {
|
||||
let amount = linear.resolve(ctx.state.font.font_size());
|
||||
let spacing = Spacing { amount, softness: Softness::Hard };
|
||||
if ctx.state.flow.main.axis() == axis {
|
||||
if axis == ctx.state.flow.main.axis() {
|
||||
ctx.end_par_group();
|
||||
ctx.push(spacing);
|
||||
ctx.start_par_group();
|
||||
@ -266,79 +243,78 @@ fn spacing(mut args: Args, ctx: &mut EvalContext, axis: SpecAxis) -> Value {
|
||||
/// `page`: Configure pages.
|
||||
///
|
||||
/// # Positional arguments
|
||||
/// - Paper name (optional, `Paper`).
|
||||
/// - Paper name: optional, of type `string`, see [here](crate::paper) for a
|
||||
/// full list of all paper names.
|
||||
///
|
||||
/// # Named arguments
|
||||
/// - `width` (`length`): The width of pages.
|
||||
/// - `height` (`length`): The height of pages.
|
||||
/// - `margins` (`linear` relative to sides): The margins for all sides.
|
||||
/// - `left` (`linear` relative to width): The left margin.
|
||||
/// - `right` (`linear` relative to width): The right margin.
|
||||
/// - `top` (`linear` relative to height): The top margin.
|
||||
/// - `bottom` (`linear` relative to height): The bottom margin.
|
||||
/// - `flip` (`bool`): Flips custom or paper-defined width and height.
|
||||
///
|
||||
/// # Enumerations
|
||||
/// - `Paper`: See [here](crate::paper) for a full list.
|
||||
pub fn page(mut args: Args, ctx: &mut EvalContext) -> Value {
|
||||
/// - Width of the page: `width`, of type `length`.
|
||||
/// - Height of the page: `height`, of type `length`.
|
||||
/// - Margins for all sides: `margins`, of type `linear` relative to sides.
|
||||
/// - Left margin: `left`, of type `linear` relative to width.
|
||||
/// - Right margin: `right`, of type `linear` relative to width.
|
||||
/// - Top margin: `top`, of type `linear` relative to height.
|
||||
/// - Bottom margin: `bottom`, of type `linear` relative to height.
|
||||
/// - Flip width and height: `flip`, of type `bool`.
|
||||
pub fn page(ctx: &mut EvalContext, args: &mut Args) -> Value {
|
||||
let snapshot = ctx.state.clone();
|
||||
let body = args.find::<SynTree>();
|
||||
|
||||
if let Some(paper) = args.get::<_, Paper>(ctx, 0) {
|
||||
ctx.state.page.class = paper.class;
|
||||
ctx.state.page.size = paper.size();
|
||||
if let Some(name) = args.find::<Spanned<String>>(ctx) {
|
||||
if let Some(paper) = Paper::from_name(&name.v) {
|
||||
ctx.state.page.class = paper.class;
|
||||
ctx.state.page.size = paper.size();
|
||||
} else {
|
||||
ctx.diag(error!(name.span, "invalid paper name"));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(width) = args.get::<_, Length>(ctx, "width") {
|
||||
if let Some(width) = args.get(ctx, "width") {
|
||||
ctx.state.page.class = PaperClass::Custom;
|
||||
ctx.state.page.size.width = width;
|
||||
}
|
||||
|
||||
if let Some(height) = args.get::<_, Length>(ctx, "height") {
|
||||
if let Some(height) = args.get(ctx, "height") {
|
||||
ctx.state.page.class = PaperClass::Custom;
|
||||
ctx.state.page.size.height = height;
|
||||
}
|
||||
|
||||
if let Some(margins) = args.get::<_, Linear>(ctx, "margins") {
|
||||
if let Some(margins) = args.get(ctx, "margins") {
|
||||
ctx.state.page.margins = Sides::uniform(Some(margins));
|
||||
}
|
||||
|
||||
if let Some(left) = args.get::<_, Linear>(ctx, "left") {
|
||||
if let Some(left) = args.get(ctx, "left") {
|
||||
ctx.state.page.margins.left = Some(left);
|
||||
}
|
||||
|
||||
if let Some(top) = args.get::<_, Linear>(ctx, "top") {
|
||||
if let Some(top) = args.get(ctx, "top") {
|
||||
ctx.state.page.margins.top = Some(top);
|
||||
}
|
||||
|
||||
if let Some(right) = args.get::<_, Linear>(ctx, "right") {
|
||||
if let Some(right) = args.get(ctx, "right") {
|
||||
ctx.state.page.margins.right = Some(right);
|
||||
}
|
||||
|
||||
if let Some(bottom) = args.get::<_, Linear>(ctx, "bottom") {
|
||||
if let Some(bottom) = args.get(ctx, "bottom") {
|
||||
ctx.state.page.margins.bottom = Some(bottom);
|
||||
}
|
||||
|
||||
if args.get::<_, bool>(ctx, "flip").unwrap_or(false) {
|
||||
if args.get(ctx, "flip").unwrap_or(false) {
|
||||
let size = &mut ctx.state.page.size;
|
||||
std::mem::swap(&mut size.width, &mut size.height);
|
||||
}
|
||||
|
||||
let main = args.get::<_, Spanned<Dir>>(ctx, "main-dir");
|
||||
let cross = args.get::<_, Spanned<Dir>>(ctx, "cross-dir");
|
||||
let main = args.get(ctx, "main-dir");
|
||||
let cross = args.get(ctx, "cross-dir");
|
||||
|
||||
ctx.set_flow(Gen::new(main, cross));
|
||||
|
||||
args.done(ctx);
|
||||
|
||||
let mut softness = ctx.end_page_group(|_| false);
|
||||
|
||||
if let Some(body) = body {
|
||||
if let Some(body) = args.find::<ValueContent>(ctx) {
|
||||
// TODO: Restrict body to a single page?
|
||||
ctx.start_page_group(Softness::Hard);
|
||||
body.eval(ctx);
|
||||
ctx.end_page_group(|s| s == Softness::Hard);
|
||||
ctx.state = snapshot;
|
||||
softness = Softness::Soft;
|
||||
ctx.state = snapshot;
|
||||
}
|
||||
|
||||
ctx.start_page_group(softness);
|
||||
@ -347,8 +323,7 @@ pub fn page(mut args: Args, ctx: &mut EvalContext) -> Value {
|
||||
}
|
||||
|
||||
/// `pagebreak`: Start a new page.
|
||||
pub fn pagebreak(args: Args, ctx: &mut EvalContext) -> Value {
|
||||
args.done(ctx);
|
||||
pub fn pagebreak(ctx: &mut EvalContext, _: &mut Args) -> Value {
|
||||
ctx.end_page_group(|_| true);
|
||||
ctx.start_page_group(Softness::Hard);
|
||||
Value::None
|
||||
|
@ -8,31 +8,60 @@ pub use insert::*;
|
||||
pub use layout::*;
|
||||
pub use style::*;
|
||||
|
||||
use crate::eval::{Scope, ValueFunc};
|
||||
use fontdock::{FontStretch, FontStyle, FontWeight};
|
||||
|
||||
macro_rules! std {
|
||||
($($func:expr $(=> $name:expr)?),* $(,)?) => {
|
||||
/// The scope containing all standard library functions.
|
||||
pub fn _std() -> Scope {
|
||||
let mut std = Scope::new();
|
||||
$(
|
||||
let _name = stringify!($func);
|
||||
$(let _name = $name;)?
|
||||
std.set(_name, ValueFunc::new($func));
|
||||
)*
|
||||
std
|
||||
}
|
||||
};
|
||||
}
|
||||
use crate::eval::Scope;
|
||||
use crate::geom::Dir;
|
||||
|
||||
std! {
|
||||
align,
|
||||
boxed => "box",
|
||||
font,
|
||||
h,
|
||||
image,
|
||||
page,
|
||||
pagebreak,
|
||||
rgb,
|
||||
v,
|
||||
/// The scope containing the standard library.
|
||||
pub fn _std() -> Scope {
|
||||
let mut std = Scope::new();
|
||||
|
||||
// Functions.
|
||||
std.set("align", align);
|
||||
std.set("box", boxed);
|
||||
std.set("font", font);
|
||||
std.set("h", h);
|
||||
std.set("image", image);
|
||||
std.set("page", page);
|
||||
std.set("pagebreak", pagebreak);
|
||||
std.set("rgb", rgb);
|
||||
std.set("v", v);
|
||||
|
||||
// Constants.
|
||||
std.set("left", Alignment::Left);
|
||||
std.set("center", Alignment::Center);
|
||||
std.set("right", Alignment::Right);
|
||||
std.set("top", Alignment::Top);
|
||||
std.set("bottom", Alignment::Bottom);
|
||||
std.set("ltr", Dir::LTR);
|
||||
std.set("rtl", Dir::RTL);
|
||||
std.set("ttb", Dir::TTB);
|
||||
std.set("btt", Dir::BTT);
|
||||
std.set("serif", FontFamily::Serif);
|
||||
std.set("sans-serif", FontFamily::SansSerif);
|
||||
std.set("monospace", FontFamily::Monospace);
|
||||
std.set("normal", FontStyle::Normal);
|
||||
std.set("italic", FontStyle::Italic);
|
||||
std.set("oblique", FontStyle::Oblique);
|
||||
std.set("thin", FontWeight::THIN);
|
||||
std.set("extralight", FontWeight::EXTRALIGHT);
|
||||
std.set("light", FontWeight::LIGHT);
|
||||
std.set("regular", FontWeight::REGULAR);
|
||||
std.set("medium", FontWeight::MEDIUM);
|
||||
std.set("semibold", FontWeight::SEMIBOLD);
|
||||
std.set("bold", FontWeight::BOLD);
|
||||
std.set("extrabold", FontWeight::EXTRABOLD);
|
||||
std.set("black", FontWeight::BLACK);
|
||||
std.set("ultra-condensed", FontStretch::UltraCondensed);
|
||||
std.set("extra-condensed", FontStretch::ExtraCondensed);
|
||||
std.set("condensed", FontStretch::Condensed);
|
||||
std.set("semi-condensed", FontStretch::SemiCondensed);
|
||||
std.set("normal", FontStretch::Normal);
|
||||
std.set("semi-expanded", FontStretch::SemiExpanded);
|
||||
std.set("expanded", FontStretch::Expanded);
|
||||
std.set("extra-expanded", FontStretch::ExtraExpanded);
|
||||
std.set("ultra-expanded", FontStretch::UltraExpanded);
|
||||
|
||||
std
|
||||
}
|
||||
|
@ -1,58 +1,41 @@
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::rc::Rc;
|
||||
|
||||
use fontdock::{FontStretch, FontStyle, FontWeight};
|
||||
|
||||
use crate::color::{Color, RgbaColor};
|
||||
use crate::eval::StringLike;
|
||||
use crate::geom::Linear;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// `font`: Configure the font.
|
||||
///
|
||||
/// # Positional arguments
|
||||
/// - Font size (optional, `linear` relative to current font size).
|
||||
/// - Font families ... (optional, variadic, `Family`)
|
||||
/// - Font size: optional, of type `linear` relative to current font size.
|
||||
/// - Font families: variadic, of type `font-family`.
|
||||
///
|
||||
/// # Named arguments
|
||||
/// - `style` (`Style`): The font style.
|
||||
/// - `weight` (`Weight`): The font weight.
|
||||
/// - `stretch` (`Stretch`): The font stretch.
|
||||
/// - `serif` (`Family` or `dict` of type `Family`): The serif family.
|
||||
/// - `sans-serif` (`Family` or `dict` of type `Family`): The new sansserif family.
|
||||
/// - `monospace` (`Family` or `dict` of type `Family`): The monospace family.
|
||||
/// - `emoji` (`Family` or `dict` of type `Family`): The emoji family.
|
||||
/// - `math` (`Family` or `dict` of type `Family`): The math family.
|
||||
/// - Font Style: `style`, of type `font-style`.
|
||||
/// - Font Weight: `weight`, of type `font-weight`.
|
||||
/// - Font Stretch: `stretch`, of type `font-stretch`.
|
||||
/// - Serif family definition: `serif`, of type `font-families`.
|
||||
/// - Sans-serif family definition: `sans-serif`, of type `font-families`.
|
||||
/// - Monospace family definition: `monospace`, of type `font-families`.
|
||||
///
|
||||
/// # Examples
|
||||
/// Set font size and font families.
|
||||
/// ```typst
|
||||
/// [font 12pt, "Arial", "Noto Sans", sans-serif]
|
||||
/// ```
|
||||
///
|
||||
/// Redefine the default sans-serif family to a single font family.
|
||||
/// ```typst
|
||||
/// [font sans-serif: "Source Sans Pro"]
|
||||
/// ```
|
||||
///
|
||||
/// Redefine the default emoji family with a fallback.
|
||||
/// ```typst
|
||||
/// [font emoji: ("Segoe UI Emoji", "Noto Emoji")]
|
||||
/// ```
|
||||
///
|
||||
/// # Enumerations
|
||||
/// - `Family`
|
||||
/// # Relevant types and constants
|
||||
/// - Type `font-families`
|
||||
/// - coerces from `string`
|
||||
/// - coerces from `array`
|
||||
/// - coerces from `font-family`
|
||||
/// - Type `font-family`
|
||||
/// - `serif`
|
||||
/// - `sans-serif`
|
||||
/// - `monospace`
|
||||
/// - `emoji`
|
||||
/// - `math`
|
||||
/// - any string
|
||||
/// - `Style`
|
||||
/// - coerces from `string`
|
||||
/// - Type `font-style`
|
||||
/// - `normal`
|
||||
/// - `italic`
|
||||
/// - `oblique`
|
||||
/// - `Weight`
|
||||
/// - `thin` or `hairline` (100)
|
||||
/// - Type `font-weight`
|
||||
/// - `thin` (100)
|
||||
/// - `extralight` (200)
|
||||
/// - `light` (300)
|
||||
/// - `regular` (400)
|
||||
@ -61,8 +44,8 @@ use crate::prelude::*;
|
||||
/// - `bold` (700)
|
||||
/// - `extrabold` (800)
|
||||
/// - `black` (900)
|
||||
/// - any integer between 100 and 900
|
||||
/// - `Stretch`
|
||||
/// - coerces from `integer`
|
||||
/// - Type `font-stretch`
|
||||
/// - `ultra-condensed`
|
||||
/// - `extra-condensed`
|
||||
/// - `condensed`
|
||||
@ -72,11 +55,10 @@ use crate::prelude::*;
|
||||
/// - `expanded`
|
||||
/// - `extra-expanded`
|
||||
/// - `ultra-expanded`
|
||||
pub fn font(mut args: Args, ctx: &mut EvalContext) -> Value {
|
||||
pub fn font(ctx: &mut EvalContext, args: &mut Args) -> Value {
|
||||
let snapshot = ctx.state.clone();
|
||||
let body = args.find::<SynTree>();
|
||||
|
||||
if let Some(linear) = args.find::<Linear>() {
|
||||
if let Some(linear) = args.find::<Linear>(ctx) {
|
||||
if linear.is_absolute() {
|
||||
ctx.state.font.size = linear.abs;
|
||||
ctx.state.font.scale = Relative::ONE.into();
|
||||
@ -85,52 +67,35 @@ pub fn font(mut args: Args, ctx: &mut EvalContext) -> Value {
|
||||
}
|
||||
}
|
||||
|
||||
let mut needs_flattening = false;
|
||||
let list: Vec<_> = args.find_all::<StringLike>().map(|s| s.to_lowercase()).collect();
|
||||
|
||||
let list: Vec<_> = args.filter::<FontFamily>(ctx).map(|f| f.to_string()).collect();
|
||||
if !list.is_empty() {
|
||||
Rc::make_mut(&mut ctx.state.font.families).list = list;
|
||||
needs_flattening = true;
|
||||
let families = Rc::make_mut(&mut ctx.state.font.families);
|
||||
families.list = list;
|
||||
families.flatten();
|
||||
}
|
||||
|
||||
if let Some(style) = args.get::<_, FontStyle>(ctx, "style") {
|
||||
if let Some(style) = args.get(ctx, "style") {
|
||||
ctx.state.font.variant.style = style;
|
||||
}
|
||||
|
||||
if let Some(weight) = args.get::<_, FontWeight>(ctx, "weight") {
|
||||
if let Some(weight) = args.get(ctx, "weight") {
|
||||
ctx.state.font.variant.weight = weight;
|
||||
}
|
||||
|
||||
if let Some(stretch) = args.get::<_, FontStretch>(ctx, "stretch") {
|
||||
if let Some(stretch) = args.get(ctx, "stretch") {
|
||||
ctx.state.font.variant.stretch = stretch;
|
||||
}
|
||||
|
||||
struct FamilyList(Vec<String>);
|
||||
|
||||
try_from_match!(FamilyList["family or list of families"] @ span:
|
||||
Value::Str(v) => Self(vec![v.to_lowercase()]),
|
||||
Value::Dict(v) => Self(Args(v.with_span(span))
|
||||
.find_all::<StringLike>()
|
||||
.map(|s| s.to_lowercase())
|
||||
.collect()
|
||||
),
|
||||
);
|
||||
|
||||
for &class in &["serif", "sans-serif", "monospace", "emoji", "math"] {
|
||||
if let Some(list) = args.get::<_, FamilyList>(ctx, class) {
|
||||
Rc::make_mut(&mut ctx.state.font.families)
|
||||
.update_class_list(class.to_string(), list.0);
|
||||
needs_flattening = true;
|
||||
for variant in FontFamily::VARIANTS {
|
||||
if let Some(FontFamilies(list)) = args.get(ctx, variant.as_str()) {
|
||||
let strings = list.into_iter().map(|f| f.to_string()).collect();
|
||||
let families = Rc::make_mut(&mut ctx.state.font.families);
|
||||
families.update_class_list(variant.to_string(), strings);
|
||||
families.flatten();
|
||||
}
|
||||
}
|
||||
|
||||
if needs_flattening {
|
||||
Rc::make_mut(&mut ctx.state.font.families).flatten();
|
||||
}
|
||||
|
||||
args.done(ctx);
|
||||
|
||||
if let Some(body) = body {
|
||||
if let Some(body) = args.find::<ValueContent>(ctx) {
|
||||
body.eval(ctx);
|
||||
ctx.state = snapshot;
|
||||
}
|
||||
@ -138,24 +103,94 @@ pub fn font(mut args: Args, ctx: &mut EvalContext) -> Value {
|
||||
Value::None
|
||||
}
|
||||
|
||||
/// A list of font families.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct FontFamilies(Vec<FontFamily>);
|
||||
|
||||
impl_type! {
|
||||
FontFamilies: "font family or array of font families",
|
||||
Value::Str(string) => Self(vec![FontFamily::Named(string.to_lowercase())]),
|
||||
Value::Array(values) => Self(values
|
||||
.into_iter()
|
||||
.filter_map(|v| v.cast().ok())
|
||||
.collect()
|
||||
),
|
||||
#(family: FontFamily) => Self(vec![family]),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub(crate) enum FontFamily {
|
||||
Serif,
|
||||
SansSerif,
|
||||
Monospace,
|
||||
Named(String),
|
||||
}
|
||||
|
||||
impl FontFamily {
|
||||
pub const VARIANTS: &'static [Self] =
|
||||
&[Self::Serif, Self::SansSerif, Self::Monospace];
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Self::Serif => "serif",
|
||||
Self::SansSerif => "sans-serif",
|
||||
Self::Monospace => "monospace",
|
||||
Self::Named(s) => s,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for FontFamily {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.pad(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl_type! {
|
||||
FontFamily: "font family",
|
||||
Value::Str(string) => Self::Named(string.to_lowercase())
|
||||
}
|
||||
|
||||
impl_type! {
|
||||
FontStyle: "font style"
|
||||
}
|
||||
|
||||
impl_type! {
|
||||
FontWeight: "font weight",
|
||||
Value::Int(number) => {
|
||||
let [min, max] = [Self::THIN, Self::BLACK];
|
||||
let message = || format!("must be between {:#?} and {:#?}", min, max);
|
||||
return if number < i64::from(min.to_number()) {
|
||||
CastResult::Warn(min, message())
|
||||
} else if number > i64::from(max.to_number()) {
|
||||
CastResult::Warn(max, message())
|
||||
} else {
|
||||
CastResult::Ok(Self::from_number(number as u16))
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
impl_type! {
|
||||
FontStretch: "font stretch"
|
||||
}
|
||||
|
||||
/// `rgb`: Create an RGB(A) color.
|
||||
///
|
||||
/// # Positional arguments
|
||||
/// - Red component (`float` between 0.0 and 1.0).
|
||||
/// - Green component (`float` between 0.0 and 1.0).
|
||||
/// - Blue component (`float` between 0.0 and 1.0).
|
||||
/// - Alpha component (optional, `float` between 0.0 and 1.0).
|
||||
pub fn rgb(mut args: Args, ctx: &mut EvalContext) -> Value {
|
||||
let r = args.need::<_, Spanned<f64>>(ctx, 0, "red component");
|
||||
let g = args.need::<_, Spanned<f64>>(ctx, 1, "green component");
|
||||
let b = args.need::<_, Spanned<f64>>(ctx, 2, "blue component");
|
||||
let a = args.get::<_, Spanned<f64>>(ctx, 3);
|
||||
args.done(ctx);
|
||||
/// - Red component: of type `float`, between 0.0 and 1.0.
|
||||
/// - Green component: of type `float`, between 0.0 and 1.0.
|
||||
/// - Blue component: of type `float`, between 0.0 and 1.0.
|
||||
/// - Alpha component: optional, of type `float`, between 0.0 and 1.0.
|
||||
pub fn rgb(ctx: &mut EvalContext, args: &mut Args) -> Value {
|
||||
let r = args.require(ctx, "red component");
|
||||
let g = args.require(ctx, "green component");
|
||||
let b = args.require(ctx, "blue component");
|
||||
let a = args.find(ctx);
|
||||
|
||||
let mut clamp = |component: Option<Spanned<f64>>, default| {
|
||||
component.map_or(default, |c| {
|
||||
if c.v < 0.0 || c.v > 1.0 {
|
||||
ctx.diag(error!(c.span, "should be between 0.0 and 1.0"));
|
||||
ctx.diag(warning!(c.span, "must be between 0.0 and 1.0"));
|
||||
}
|
||||
(c.v.max(0.0).min(1.0) * 255.0).round() as u8
|
||||
})
|
||||
|
142
src/parse/collection.rs
Normal file
142
src/parse/collection.rs
Normal file
@ -0,0 +1,142 @@
|
||||
use super::*;
|
||||
use crate::diag::Deco;
|
||||
|
||||
/// Parse the arguments to a function call.
|
||||
pub fn arguments(p: &mut Parser) -> Arguments {
|
||||
collection(p, vec![])
|
||||
}
|
||||
|
||||
/// Parse a parenthesized group, which can be either of:
|
||||
/// - Array literal
|
||||
/// - Dictionary literal
|
||||
/// - Parenthesized expression
|
||||
pub fn parenthesized(p: &mut Parser) -> Expr {
|
||||
p.start_group(Group::Paren);
|
||||
let state = if p.eat_if(Token::Colon) {
|
||||
collection(p, State::Dict(vec![]))
|
||||
} else {
|
||||
collection(p, State::Unknown)
|
||||
};
|
||||
p.end_group();
|
||||
state.into_expr()
|
||||
}
|
||||
|
||||
/// Parse a collection.
|
||||
fn collection<T: Collection>(p: &mut Parser, mut collection: T) -> T {
|
||||
let mut missing_coma = None;
|
||||
|
||||
while !p.eof() {
|
||||
if let Some(arg) = p.span_if(argument) {
|
||||
collection.push_arg(p, arg);
|
||||
|
||||
if let Some(pos) = missing_coma.take() {
|
||||
p.diag_expected_at("comma", pos);
|
||||
}
|
||||
|
||||
if p.eof() {
|
||||
break;
|
||||
}
|
||||
|
||||
let behind = p.last_end();
|
||||
if p.eat_if(Token::Comma) {
|
||||
collection.push_comma();
|
||||
} else {
|
||||
missing_coma = Some(behind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collection
|
||||
}
|
||||
|
||||
/// Parse an expression or a named pair.
|
||||
fn argument(p: &mut Parser) -> Option<Argument> {
|
||||
let first = p.span_if(expr)?;
|
||||
if p.eat_if(Token::Colon) {
|
||||
if let Expr::Lit(Lit::Ident(ident)) = first.v {
|
||||
let expr = p.span_if(expr)?;
|
||||
let name = ident.with_span(first.span);
|
||||
p.deco(Deco::Name.with_span(name.span));
|
||||
Some(Argument::Named(Named { name, expr }))
|
||||
} else {
|
||||
p.diag(error!(first.span, "name must be identifier"));
|
||||
expr(p);
|
||||
None
|
||||
}
|
||||
} else {
|
||||
Some(Argument::Pos(first))
|
||||
}
|
||||
}
|
||||
|
||||
/// Abstraction for comma-separated list of expression / named pairs.
|
||||
trait Collection {
|
||||
fn push_arg(&mut self, p: &mut Parser, arg: Spanned<Argument>);
|
||||
fn push_comma(&mut self) {}
|
||||
}
|
||||
|
||||
impl Collection for Arguments {
|
||||
fn push_arg(&mut self, _: &mut Parser, arg: Spanned<Argument>) {
|
||||
self.push(arg.v);
|
||||
}
|
||||
}
|
||||
|
||||
/// State of collection parsing.
|
||||
#[derive(Debug)]
|
||||
enum State {
|
||||
Unknown,
|
||||
Expr(Spanned<Expr>),
|
||||
Array(Array),
|
||||
Dict(Dict),
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn into_expr(self) -> Expr {
|
||||
match self {
|
||||
Self::Unknown => Expr::Lit(Lit::Array(vec![])),
|
||||
Self::Expr(expr) => expr.v,
|
||||
Self::Array(array) => Expr::Lit(Lit::Array(array)),
|
||||
Self::Dict(dict) => Expr::Lit(Lit::Dict(dict)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Collection for State {
|
||||
fn push_arg(&mut self, p: &mut Parser, arg: Spanned<Argument>) {
|
||||
match self {
|
||||
Self::Unknown => match arg.v {
|
||||
Argument::Pos(expr) => *self = Self::Expr(expr),
|
||||
Argument::Named(named) => *self = Self::Dict(vec![named]),
|
||||
},
|
||||
Self::Expr(prev) => match arg.v {
|
||||
Argument::Pos(expr) => *self = Self::Array(vec![take(prev), expr]),
|
||||
Argument::Named(_) => diag(p, arg),
|
||||
},
|
||||
Self::Array(array) => match arg.v {
|
||||
Argument::Pos(expr) => array.push(expr),
|
||||
Argument::Named(_) => diag(p, arg),
|
||||
},
|
||||
Self::Dict(dict) => match arg.v {
|
||||
Argument::Pos(_) => diag(p, arg),
|
||||
Argument::Named(named) => dict.push(named),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn push_comma(&mut self) {
|
||||
if let Self::Expr(expr) = self {
|
||||
*self = Self::Array(vec![take(expr)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn take(expr: &mut Spanned<Expr>) -> Spanned<Expr> {
|
||||
// Replace with anything, it's overwritten anyway.
|
||||
std::mem::replace(expr, Spanned::zero(Expr::Lit(Lit::Bool(false))))
|
||||
}
|
||||
|
||||
fn diag(p: &mut Parser, arg: Spanned<Argument>) {
|
||||
p.diag(error!(arg.span, "{}", match arg.v {
|
||||
Argument::Pos(_) => "expected named pair, found expression",
|
||||
Argument::Named(_) => "expected expression, found named pair",
|
||||
}));
|
||||
}
|
105
src/parse/mod.rs
105
src/parse/mod.rs
@ -1,5 +1,6 @@
|
||||
//! Parsing and tokenization.
|
||||
|
||||
mod collection;
|
||||
mod lines;
|
||||
mod parser;
|
||||
mod resolve;
|
||||
@ -15,10 +16,11 @@ pub use tokens::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::color::RgbaColor;
|
||||
use crate::diag::{Deco, Pass};
|
||||
use crate::eval::DictKey;
|
||||
use crate::diag::Pass;
|
||||
use crate::syntax::*;
|
||||
|
||||
use collection::{arguments, parenthesized};
|
||||
|
||||
/// Parse a string of source code.
|
||||
pub fn parse(src: &str) -> Pass<SynTree> {
|
||||
let mut p = Parser::new(src);
|
||||
@ -153,6 +155,9 @@ fn block_expr(p: &mut Parser) -> Option<Expr> {
|
||||
p.push_mode(TokenMode::Header);
|
||||
p.start_group(Group::Brace);
|
||||
let expr = expr(p);
|
||||
while !p.eof() {
|
||||
p.diag_unexpected();
|
||||
}
|
||||
p.pop_mode();
|
||||
p.end_group();
|
||||
expr
|
||||
@ -161,7 +166,7 @@ fn block_expr(p: &mut Parser) -> Option<Expr> {
|
||||
/// Parse a parenthesized function call.
|
||||
fn paren_call(p: &mut Parser, name: Spanned<Ident>) -> ExprCall {
|
||||
p.start_group(Group::Paren);
|
||||
let args = p.span(|p| dict_contents(p).0);
|
||||
let args = p.span(arguments);
|
||||
p.end_group();
|
||||
ExprCall { name, args }
|
||||
}
|
||||
@ -184,16 +189,16 @@ fn bracket_call(p: &mut Parser) -> ExprCall {
|
||||
p.end_group();
|
||||
|
||||
if p.peek() == Some(Token::LeftBracket) {
|
||||
let expr = p.span(|p| Expr::Lit(Lit::Content(bracket_body(p))));
|
||||
inner.span.expand(expr.span);
|
||||
inner.v.args.v.0.push(LitDictEntry { key: None, expr });
|
||||
let body = p.span(|p| Expr::Lit(Lit::Content(bracket_body(p))));
|
||||
inner.span.expand(body.span);
|
||||
inner.v.args.v.push(Argument::Pos(body));
|
||||
}
|
||||
|
||||
while let Some(mut top) = outer.pop() {
|
||||
let span = inner.span;
|
||||
let node = inner.map(|c| SynNode::Expr(Expr::Call(c)));
|
||||
let expr = Expr::Lit(Lit::Content(vec![node])).with_span(span);
|
||||
top.v.args.v.0.push(LitDictEntry { key: None, expr });
|
||||
top.v.args.v.push(Argument::Pos(expr));
|
||||
inner = top;
|
||||
}
|
||||
|
||||
@ -215,9 +220,9 @@ fn bracket_subheader(p: &mut Parser) -> ExprCall {
|
||||
Ident(String::new()).with_span(start)
|
||||
});
|
||||
|
||||
let args = p.span(|p| dict_contents(p).0);
|
||||
|
||||
let args = p.span(arguments);
|
||||
p.end_group();
|
||||
|
||||
ExprCall { name, args }
|
||||
}
|
||||
|
||||
@ -231,75 +236,6 @@ fn bracket_body(p: &mut Parser) -> SynTree {
|
||||
tree
|
||||
}
|
||||
|
||||
/// Parse the contents of a dictionary.
|
||||
fn dict_contents(p: &mut Parser) -> (LitDict, bool) {
|
||||
let mut dict = LitDict::new();
|
||||
let mut missing_coma = None;
|
||||
let mut comma_and_keyless = true;
|
||||
|
||||
while !p.eof() {
|
||||
if let Some(entry) = dict_entry(p) {
|
||||
let behind = entry.expr.span.end;
|
||||
if let Some(pos) = missing_coma.take() {
|
||||
p.diag_expected_at("comma", pos);
|
||||
}
|
||||
|
||||
if let Some(key) = &entry.key {
|
||||
comma_and_keyless = false;
|
||||
p.deco(Deco::Name.with_span(key.span));
|
||||
}
|
||||
|
||||
dict.0.push(entry);
|
||||
if p.eof() {
|
||||
break;
|
||||
}
|
||||
|
||||
if p.eat_if(Token::Comma) {
|
||||
comma_and_keyless = false;
|
||||
} else {
|
||||
missing_coma = Some(behind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let coercible = comma_and_keyless && !dict.0.is_empty();
|
||||
(dict, coercible)
|
||||
}
|
||||
|
||||
/// Parse a single entry in a dictionary.
|
||||
fn dict_entry(p: &mut Parser) -> Option<LitDictEntry> {
|
||||
if let Some(ident) = p.span_if(ident) {
|
||||
match p.peek() {
|
||||
// Key-value pair.
|
||||
Some(Token::Colon) => {
|
||||
p.eat_assert(Token::Colon);
|
||||
p.span_if(expr).map(|expr| LitDictEntry {
|
||||
key: Some(ident.map(|id| DictKey::Str(id.0))),
|
||||
expr,
|
||||
})
|
||||
}
|
||||
|
||||
// Function call.
|
||||
Some(Token::LeftParen) => Some(LitDictEntry {
|
||||
key: None,
|
||||
expr: {
|
||||
let start = ident.span.start;
|
||||
let call = paren_call(p, ident);
|
||||
Expr::Call(call).with_span(start .. p.last_end())
|
||||
},
|
||||
}),
|
||||
|
||||
// Just an identifier.
|
||||
_ => Some(LitDictEntry {
|
||||
key: None,
|
||||
expr: ident.map(|id| Expr::Lit(Lit::Ident(id))),
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
p.span_if(expr).map(|expr| LitDictEntry { key: None, expr })
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse an expression: `term (+ term)*`.
|
||||
fn expr(p: &mut Parser) -> Option<Expr> {
|
||||
binops(p, term, |token| match token {
|
||||
@ -418,19 +354,6 @@ fn content(p: &mut Parser) -> SynTree {
|
||||
tree
|
||||
}
|
||||
|
||||
/// Parse a parenthesized expression: `(a + b)`, `(1, name: "value").
|
||||
fn parenthesized(p: &mut Parser) -> Expr {
|
||||
p.start_group(Group::Paren);
|
||||
let (dict, coercible) = dict_contents(p);
|
||||
let expr = if coercible {
|
||||
dict.0.into_iter().next().expect("dict is coercible").expr.v
|
||||
} else {
|
||||
Expr::Lit(Lit::Dict(dict))
|
||||
};
|
||||
p.end_group();
|
||||
expr
|
||||
}
|
||||
|
||||
/// Parse an identifier.
|
||||
fn ident(p: &mut Parser) -> Option<Ident> {
|
||||
p.eat_map(|token| match token {
|
||||
|
@ -5,7 +5,6 @@ use std::fmt::Debug;
|
||||
use super::parse;
|
||||
use crate::color::RgbaColor;
|
||||
use crate::diag::{Diag, Level, Pass};
|
||||
use crate::eval::DictKey;
|
||||
use crate::geom::Unit;
|
||||
use crate::syntax::*;
|
||||
|
||||
@ -154,21 +153,38 @@ fn Unary(op: impl Into<Spanned<UnOp>>, expr: impl Into<Spanned<Expr>>) -> Expr {
|
||||
})
|
||||
}
|
||||
|
||||
macro_rules! Array {
|
||||
(@$($expr:expr),* $(,)?) => {
|
||||
vec![$(into!($expr)),*]
|
||||
};
|
||||
($($tts:tt)*) => (Expr::Lit(Lit::Array(Array![@$($tts)*])));
|
||||
}
|
||||
|
||||
macro_rules! Dict {
|
||||
(@$($a:expr $(=> $b:expr)?),* $(,)?) => {
|
||||
LitDict(vec![$(#[allow(unused)] {
|
||||
let key: Option<Spanned<DictKey>> = None;
|
||||
let expr = $a;
|
||||
$(
|
||||
let key = Some(into!($a).map(|s: &str| s.into()));
|
||||
let expr = $b;
|
||||
)?
|
||||
LitDictEntry { key, expr: into!(expr) }
|
||||
}),*])
|
||||
(@$($name:expr => $expr:expr),* $(,)?) => {
|
||||
vec![$(Named {
|
||||
name: into!($name).map(|s: &str| Ident(s.into())),
|
||||
expr: into!($expr)
|
||||
}),*]
|
||||
};
|
||||
($($tts:tt)*) => (Expr::Lit(Lit::Dict(Dict![@$($tts)*])));
|
||||
}
|
||||
|
||||
macro_rules! Args {
|
||||
(@$a:expr) => {
|
||||
Argument::Pos(into!($a))
|
||||
};
|
||||
(@$a:expr => $b:expr) => {
|
||||
Argument::Named(Named {
|
||||
name: into!($a).map(|s: &str| Ident(s.into())),
|
||||
expr: into!($b)
|
||||
})
|
||||
};
|
||||
($($a:expr $(=> $b:expr)?),* $(,)?) => {
|
||||
vec![$(Args!(@$a $(=> $b)?)),*]
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! Content {
|
||||
(@$($node:expr),* $(,)?) => (vec![$(into!($node)),*]);
|
||||
($($tts:tt)*) => (Expr::Lit(Lit::Content(Content![@$($tts)*])));
|
||||
@ -188,10 +204,6 @@ macro_rules! Call {
|
||||
($($tts:tt)*) => (SynNode::Expr(Call!(@$($tts)*)));
|
||||
}
|
||||
|
||||
macro_rules! Args {
|
||||
($($tts:tt)*) => (Dict![@$($tts)*]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_comments() {
|
||||
// In body.
|
||||
@ -316,10 +328,9 @@ fn test_parse_groups() {
|
||||
errors: [S(1..2, "expected function name, found closing paren"),
|
||||
S(2..2, "expected closing bracket")]);
|
||||
|
||||
t!("[v {]}"
|
||||
nodes: [Call!("v", Args![Content![]])],
|
||||
errors: [S(4..4, "expected closing brace"),
|
||||
S(5..6, "unexpected closing brace")]);
|
||||
t!("[v {*]_"
|
||||
nodes: [Call!("v", Args![Content![Strong]]), Emph],
|
||||
errors: [S(5..5, "expected closing brace")]);
|
||||
|
||||
// Test brace group.
|
||||
t!("{1 + [}"
|
||||
@ -329,7 +340,7 @@ fn test_parse_groups() {
|
||||
|
||||
// Test subheader group.
|
||||
t!("[v (|u )]"
|
||||
nodes: [Call!("v", Args![Dict![], Content![Call!("u")]])],
|
||||
nodes: [Call!("v", Args![Array![], Content![Call!("u")]])],
|
||||
errors: [S(4..4, "expected closing paren"),
|
||||
S(7..8, "expected expression, found closing paren")]);
|
||||
}
|
||||
@ -348,6 +359,12 @@ fn test_parse_blocks() {
|
||||
nodes: [],
|
||||
errors: [S(1..1, "expected expression"),
|
||||
S(3..5, "expected expression, found invalid token")]);
|
||||
|
||||
// Too much stuff.
|
||||
t!("{1 #{} end"
|
||||
nodes: [Block(Int(1)), Space, Text("end")],
|
||||
errors: [S(3..4, "unexpected hex value"),
|
||||
S(4..5, "unexpected opening brace")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -385,7 +402,7 @@ fn test_parse_bracket_funcs() {
|
||||
nodes: [Call!("", Args![Int(1)])],
|
||||
errors: [S(1..2, "expected function name, found hex value")]);
|
||||
|
||||
// String header eats closing bracket.
|
||||
// String in header eats closing bracket.
|
||||
t!(r#"[v "]"#
|
||||
nodes: [Call!("v", Args![Str("]")])],
|
||||
errors: [S(5..5, "expected quote"),
|
||||
@ -400,8 +417,8 @@ fn test_parse_bracket_funcs() {
|
||||
#[test]
|
||||
fn test_parse_chaining() {
|
||||
// Basic.
|
||||
t!("[a | b]" Call!("a", Args![Content![Call!("b")]]));
|
||||
t!("[a | b | c]" Call!("a", Args![Content![
|
||||
t!("[a | b]" Call!("a", Args![Content![Call!("b")]]));
|
||||
t!("[a|b|c]" Call!("a", Args![Content![
|
||||
Call!("b", Args![Content![Call!("c")]])
|
||||
]]));
|
||||
|
||||
@ -428,16 +445,14 @@ fn test_parse_chaining() {
|
||||
#[test]
|
||||
fn test_parse_arguments() {
|
||||
// Bracket functions.
|
||||
t!("[v 1]" Call!("v", Args![Int(1)]));
|
||||
t!("[v 1,]" Call!("v", Args![Int(1)]));
|
||||
t!("[v a]" Call!("v", Args![Id("a")]));
|
||||
t!("[v a,]" Call!("v", Args![Id("a")]));
|
||||
t!("[v 1,]" Call!("v", Args![Int(1)]));
|
||||
t!("[v a:2]" Call!("v", Args!["a" => Int(2)]));
|
||||
|
||||
// Parenthesized function with nested dictionary literal.
|
||||
// Parenthesized function with nested array literal.
|
||||
t!(r#"{f(1, a: (2, 3), #004, b: "five")}"# Block(Call!(@"f", Args![
|
||||
Int(1),
|
||||
"a" => Dict![Int(2), Int(3)],
|
||||
"a" => Array![Int(2), Int(3)],
|
||||
Color(RgbaColor::new(0, 0, 0x44, 0xff)),
|
||||
"b" => Str("five"),
|
||||
])));
|
||||
@ -447,56 +462,111 @@ fn test_parse_arguments() {
|
||||
nodes: [Call!("v", Args![])],
|
||||
errors: [S(3..5, "expected expression, found end of block comment")]);
|
||||
|
||||
// Bad expression.
|
||||
t!("[v a:1:]"
|
||||
nodes: [Call!("v", Args!["a" => Int(1)])],
|
||||
errors: [S(6..7, "expected expression, found colon")]);
|
||||
|
||||
// Missing comma between arguments.
|
||||
t!("[v 1 2]"
|
||||
nodes: [Call!("v", Args![Int(1), Int(2)])],
|
||||
errors: [S(4..4, "expected comma")]);
|
||||
|
||||
// Missing expression after name.
|
||||
t!("[v a:]"
|
||||
nodes: [Call!("v", Args![])],
|
||||
errors: [S(5..5, "expected expression")]);
|
||||
|
||||
// Bad expression after name.
|
||||
t!("[v a:1:]"
|
||||
nodes: [Call!("v", Args!["a" => Int(1)])],
|
||||
errors: [S(6..7, "expected expression, found colon")]);
|
||||
|
||||
// Name has to be identifier. Number parsed as positional argument.
|
||||
// Name has to be identifier.
|
||||
t!("[v 1:]"
|
||||
nodes: [Call!("v", Args![Int(1)])],
|
||||
errors: [S(4..5, "expected expression, found colon")]);
|
||||
nodes: [Call!("v", Args![])],
|
||||
errors: [S(3..4, "name must be identifier"),
|
||||
S(5..5, "expected expression")]);
|
||||
|
||||
// Parsed as two positional arguments.
|
||||
// Name has to be identifier.
|
||||
t!("[v 1:2]"
|
||||
nodes: [Call!("v", Args![Int(1), Int(2)])],
|
||||
errors: [S(4..5, "expected expression, found colon"),
|
||||
S(4..4, "expected comma")]);
|
||||
nodes: [Call!("v", Args![])],
|
||||
errors: [S(3..4, "name must be identifier")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dict_literals() {
|
||||
// Basic.
|
||||
t!("{()}" Block(Dict![]));
|
||||
fn test_parse_arrays() {
|
||||
// Empty array.
|
||||
t!("{()}" Block(Array![]));
|
||||
|
||||
// With spans.
|
||||
t!("{(1, two: 2)}"
|
||||
nodes: [S(0..13, Block(Dict![
|
||||
S(2..3, Int(1)),
|
||||
S(5..8, "two") => S(10..11, Int(2)),
|
||||
]))],
|
||||
// Array with one item and trailing comma + spans.
|
||||
t!("{-(1,)}"
|
||||
nodes: [S(0..7, Block(Unary(
|
||||
S(1..2, Neg),
|
||||
S(2..6, Array![S(3..4, Int(1))])
|
||||
)))],
|
||||
spans: true);
|
||||
|
||||
// Array with three items and trailing comma.
|
||||
t!(r#"{("one", 2, #003,)}"# Block(Array![
|
||||
Str("one"),
|
||||
Int(2),
|
||||
Color(RgbaColor::new(0, 0, 0x33, 0xff))
|
||||
]));
|
||||
|
||||
// Unclosed.
|
||||
t!("{(}"
|
||||
nodes: [Block(Dict![])],
|
||||
nodes: [Block(Array![])],
|
||||
errors: [S(2..2, "expected closing paren")]);
|
||||
|
||||
// Missing comma + invalid token.
|
||||
t!("{(1*/2)}"
|
||||
nodes: [Block(Array![Int(1), Int(2)])],
|
||||
errors: [S(3..5, "expected expression, found end of block comment"),
|
||||
S(3..3, "expected comma")]);
|
||||
|
||||
// Invalid token.
|
||||
t!("{(1, 1u 2)}"
|
||||
nodes: [Block(Array![Int(1), Int(2)])],
|
||||
errors: [S(5..7, "expected expression, found invalid token")]);
|
||||
|
||||
// Coerced to expression with leading comma.
|
||||
t!("{(,1)}"
|
||||
nodes: [Block(Int(1))],
|
||||
errors: [S(2..3, "expected expression, found comma")]);
|
||||
|
||||
// Missing expression after name makes this an array.
|
||||
t!("{(a:)}"
|
||||
nodes: [Block(Array![])],
|
||||
errors: [S(4..4, "expected expression")]);
|
||||
|
||||
// Expected expression, found named pair.
|
||||
t!("{(1, b: 2)}"
|
||||
nodes: [Block(Array![Int(1)])],
|
||||
errors: [S(5..9, "expected expression, found named pair")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dictionaries() {
|
||||
// Empty dictionary.
|
||||
t!("{(:)}" Block(Dict![]));
|
||||
|
||||
// Dictionary with two pairs + spans.
|
||||
t!("{(one: 1, two: 2)}"
|
||||
nodes: [S(0..18, Block(Dict![
|
||||
S(2..5, "one") => S(7..8, Int(1)),
|
||||
S(10..13, "two") => S(15..16, Int(2)),
|
||||
]))],
|
||||
spans: true);
|
||||
|
||||
// Expected named pair, found expression.
|
||||
t!("{(a: 1, b)}"
|
||||
nodes: [Block(Dict!["a" => Int(1)])],
|
||||
errors: [S(8..9, "expected named pair, found expression")]);
|
||||
|
||||
// Dictionary marker followed by more stuff.
|
||||
t!("{(:1 b:2, true::)}"
|
||||
nodes: [Block(Dict!["b" => Int(2)])],
|
||||
errors: [S(3..4, "expected named pair, found expression"),
|
||||
S(4..4, "expected comma"),
|
||||
S(10..14, "name must be identifier"),
|
||||
S(15..16, "expected expression, found colon")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_expressions() {
|
||||
// Parenthesis.
|
||||
t!("{(x)}" Block(Id("x")));
|
||||
// Parentheses.
|
||||
t!("{(x)}{(1)}" Block(Id("x")), Block(Int(1)));
|
||||
|
||||
// Unary operations.
|
||||
t!("{-1}" Block(Unary(Neg, Int(1))));
|
||||
@ -561,4 +631,12 @@ fn test_parse_values() {
|
||||
t!("{#a5}"
|
||||
nodes: [Block(Color(RgbaColor::new(0, 0, 0, 0xff)))],
|
||||
errors: [S(1..4, "invalid color")]);
|
||||
|
||||
// Content.
|
||||
t!("{{*Hi*}}" Block(Content![Strong, Text("Hi"), Strong]));
|
||||
|
||||
// Invalid tokens.
|
||||
t!("{1u}"
|
||||
nodes: [],
|
||||
errors: [S(1..3, "expected expression, found invalid token")]);
|
||||
}
|
||||
|
@ -477,13 +477,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_length_from_str_parses_correct_value_and_unit() {
|
||||
fn test_length_from_str() {
|
||||
assert_eq!(parse_length("2.5cm"), Some((2.5, Cm)));
|
||||
assert_eq!(parse_length("1.e+2cm"), Some((100.0, Cm)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_length_from_str_works_with_non_ascii_chars() {
|
||||
assert_eq!(parse_length("123🚚"), None);
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,10 @@
|
||||
|
||||
pub use crate::diag::{Feedback, Pass};
|
||||
#[doc(no_inline)]
|
||||
pub use crate::eval::{Args, Dict, Eval, EvalContext, Value, ValueDict};
|
||||
pub use crate::eval::{
|
||||
Args, CastResult, Eval, EvalContext, Value, ValueAny, ValueArray, ValueContent,
|
||||
ValueDict,
|
||||
};
|
||||
pub use crate::geom::*;
|
||||
#[doc(no_inline)]
|
||||
pub use crate::layout::LayoutNode;
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
use super::*;
|
||||
use crate::color::RgbaColor;
|
||||
use crate::eval::DictKey;
|
||||
use crate::geom::Unit;
|
||||
|
||||
/// An expression.
|
||||
@ -24,10 +23,22 @@ pub struct ExprCall {
|
||||
/// The name of the function.
|
||||
pub name: Spanned<Ident>,
|
||||
/// The arguments to the function.
|
||||
///
|
||||
/// In case of a bracketed invocation with a body, the body is _not_
|
||||
/// included in the span for the sake of clearer error messages.
|
||||
pub args: Spanned<LitDict>,
|
||||
pub args: Spanned<Arguments>,
|
||||
}
|
||||
|
||||
/// The arguments to a function: `12, draw: false`.
|
||||
///
|
||||
/// In case of a bracketed invocation with a body, the body is _not_
|
||||
/// included in the span for the sake of clearer error messages.
|
||||
pub type Arguments = Vec<Argument>;
|
||||
|
||||
/// An argument to a function call: `12` or `draw: false`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Argument {
|
||||
/// A positional arguments.
|
||||
Pos(Spanned<Expr>),
|
||||
/// A named argument.
|
||||
Named(Named),
|
||||
}
|
||||
|
||||
/// A unary operation: `-x`.
|
||||
@ -92,28 +103,25 @@ pub enum Lit {
|
||||
Color(RgbaColor),
|
||||
/// A string literal: `"hello!"`.
|
||||
Str(String),
|
||||
/// A dictionary literal: `(false, 12cm, greeting: "hi")`.
|
||||
Dict(LitDict),
|
||||
/// An array literal: `(1, "hi", 12cm)`.
|
||||
Array(Array),
|
||||
/// A dictionary literal: `(color: #f79143, pattern: dashed)`.
|
||||
Dict(Dict),
|
||||
/// A content literal: `{*Hello* there!}`.
|
||||
Content(SynTree),
|
||||
}
|
||||
|
||||
/// A dictionary literal: `(false, 12cm, greeting: "hi")`.
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct LitDict(pub Vec<LitDictEntry>);
|
||||
/// An array literal: `(1, "hi", 12cm)`.
|
||||
pub type Array = SpanVec<Expr>;
|
||||
|
||||
/// An entry in a dictionary literal: `false` or `greeting: "hi"`.
|
||||
/// A dictionary literal: `(color: #f79143, pattern: dashed)`.
|
||||
pub type Dict = Vec<Named>;
|
||||
|
||||
/// A pair of a name and an expression: `pattern: dashed`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct LitDictEntry {
|
||||
/// The key of the entry if there was one: `greeting`.
|
||||
pub key: Option<Spanned<DictKey>>,
|
||||
/// The value of the entry: `"hi"`.
|
||||
pub struct Named {
|
||||
/// The name: `pattern`.
|
||||
pub name: Spanned<Ident>,
|
||||
/// The right-hand side of the pair: `dashed`.
|
||||
pub expr: Spanned<Expr>,
|
||||
}
|
||||
|
||||
impl LitDict {
|
||||
/// Create an empty dict literal.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ pub enum SynNode {
|
||||
Expr(Expr),
|
||||
}
|
||||
|
||||
/// A section heading: `# ...`.
|
||||
/// A section heading: `# Introduction`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct NodeHeading {
|
||||
/// The section depth (numer of hashtags minus 1).
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.7 KiB |
@ -6,12 +6,16 @@
|
||||
// Wrong types.
|
||||
[font style: bold, weight: "thin", serif: 0]
|
||||
|
||||
// Weight out of range.
|
||||
[font weight: 2700]
|
||||
|
||||
// Non-existing argument.
|
||||
[font something: "invalid"]
|
||||
|
||||
// compare-ref: false
|
||||
// error: 4:7-4:12 unexpected argument
|
||||
// error: 7:14-7:18 invalid font style
|
||||
// error: 7:14-7:18 expected font style, found font weight
|
||||
// error: 7:28-7:34 expected font weight, found string
|
||||
// error: 7:43-7:44 expected family or list of families, found integer
|
||||
// error: 10:7-10:27 unexpected argument
|
||||
// error: 7:43-7:44 expected font family or array of font families, found integer
|
||||
// warning: 10:15-10:19 must be between 100 and 900
|
||||
// error: 13:7-13:27 unexpected argument
|
||||
|
@ -4,15 +4,17 @@
|
||||
Emoji: 🏀
|
||||
|
||||
// CMU Serif + Noto Emoji.
|
||||
[font "CMU Serif", "Noto Emoji"][Emoji: 🏀]
|
||||
[font "CMU Serif", "Noto Emoji"][
|
||||
Emoji: 🏀
|
||||
]
|
||||
|
||||
// Class definitions.
|
||||
[font math: ("CMU Serif", "Latin Modern Math", "Noto Emoji")]
|
||||
[font math][Math: ∫ α + β ➗ 3]
|
||||
[font serif: ("CMU Serif", "Latin Modern Math", "Noto Emoji")]
|
||||
[font serif][
|
||||
Math: ∫ α + β ➗ 3
|
||||
]
|
||||
|
||||
// Class redefinition.
|
||||
// Class definition reused.
|
||||
[font sans-serif: "Noto Emoji"]
|
||||
[font sans-serif: ("Archivo", sans-serif)]
|
||||
New sans-serif. 🚀
|
||||
|
||||
// TODO: Add tests for other scripts.
|
||||
|
@ -7,5 +7,5 @@
|
||||
[page main-dir: ltr]
|
||||
|
||||
// compare-ref: false
|
||||
// error: 4:7-4:18 invalid paper
|
||||
// error: 4:7-4:18 unknown variable
|
||||
// error: 7:17-7:20 aligned axis
|
||||
|
@ -22,4 +22,4 @@
|
||||
[page margins: 0pt, left: 40pt][Overriden]
|
||||
|
||||
// Flip the page.
|
||||
[page a10, flip: true][Flipped]
|
||||
[page "a10", flip: true][Flipped]
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Test the `rgb` function.
|
||||
|
||||
// Check the output.
|
||||
[rgb 0.0, 0.3, 0.7] [val #004db3]
|
||||
[rgb 0.0, 0.3, 0.7]
|
||||
|
||||
// Alpha channel.
|
||||
[rgb 1.0, 0.0, 0.0, 0.5]
|
||||
@ -15,9 +15,8 @@
|
||||
// Missing all components.
|
||||
[rgb]
|
||||
|
||||
// error: 4:22-4:25 unknown function
|
||||
// error: 10:6-10:9 should be between 0.0 and 1.0
|
||||
// error: 10:11-10:15 should be between 0.0 and 1.0
|
||||
// warning: 10:6-10:9 must be between 0.0 and 1.0
|
||||
// warning: 10:11-10:15 must be between 0.0 and 1.0
|
||||
// error: 13:6-13:10 missing argument: blue component
|
||||
// error: 16:5-16:5 missing argument: red component
|
||||
// error: 16:5-16:5 missing argument: green component
|
||||
|
Loading…
x
Reference in New Issue
Block a user