Add Value type and replace dyn-nodes with call-exprs 🏗

- In addition to syntax trees there are now `Value`s, which syntax trees can be evaluated into (e.g. the tree is `5+5` and the value is `10`)
- Parsing is completely pure, function calls are not parsed into nodes, but into simple call expressions, which are resolved later
- Functions aren't dynamic nodes anymore, but simply functions which receive their arguments as a table and the layouting context
- Functions may return any `Value`
- Layouting is powered by functions which return the new `Commands` value, which informs the layouting engine what to do
- When a function returns a non-`Commands` value, the layouter simply dumps the value into the document in monospace
This commit is contained in:
Laurenz 2020-08-16 22:14:27 +02:00
parent 9f6137d8a8
commit 30f16bbf64
27 changed files with 1250 additions and 1341 deletions

View File

@ -11,7 +11,6 @@ members = ["main"]
opt-level = 2
[dependencies]
async-trait = "0.1"
fontdock = { path = "../fontdock" }
tide = { path = "../tide" }
ttf-parser = "0.8.2"

View File

@ -1,22 +1,18 @@
use criterion::{criterion_group, criterion_main, Criterion};
use typstc::library::_std;
use typstc::syntax::parsing::{parse, ParseState};
use typstc::syntax::parsing::parse;
use typstc::syntax::span::Pos;
// 28 not too dense lines.
const COMA: &str = include_str!("../tests/coma.typ");
fn parsing_benchmark(c: &mut Criterion) {
let state = ParseState { scope: _std() };
c.bench_function("parse-coma-28-lines", |b| {
b.iter(|| parse(COMA, Pos::ZERO, &state))
b.iter(|| parse(COMA, Pos::ZERO))
});
// 2800 lines of Typst code.
let long = COMA.repeat(100);
c.bench_function("parse-coma-2800-lines", |b| {
b.iter(|| parse(&long, Pos::ZERO, &state))
b.iter(|| parse(&long, Pos::ZERO))
});
}

131
src/color.rs Normal file
View File

@ -0,0 +1,131 @@
//! Color handling.
use std::fmt::{self, Debug, Formatter};
use std::str::FromStr;
/// An 8-bit RGBA color.
///
/// # Example
/// ```typst
/// [page: background=#423abaff]
/// ^^^^^^^^
/// ```
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct RgbaColor {
/// Red channel.
pub r: u8,
/// Green channel.
pub g: u8,
/// Blue channel.
pub b: u8,
/// Alpha channel.
pub a: u8,
/// This is true if this value was provided as a fail-over by the parser
/// because the user-defined value was invalid. This color may be
/// overwritten if this property is true.
pub healed: bool,
}
impl RgbaColor {
/// Constructs a new color.
pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a, healed: false }
}
/// Constructs a new color with the healed property set to true.
pub fn new_healed(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a, healed: true }
}
}
impl FromStr for RgbaColor {
type Err = ParseColorError;
/// Constructs a new color from a hex string like `7a03c2`. Do not specify a
/// leading `#`.
fn from_str(hex_str: &str) -> Result<Self, Self::Err> {
if !hex_str.is_ascii() {
return Err(ParseColorError);
}
let len = hex_str.len();
let long = len == 6 || len == 8;
let short = len == 3 || len == 4;
let alpha = len == 4 || len == 8;
if !long && !short {
return Err(ParseColorError);
}
let mut values: [u8; 4] = [255; 4];
for elem in if alpha { 0..4 } else { 0..3 } {
let item_len = if long { 2 } else { 1 };
let pos = elem * item_len;
let item = &hex_str[pos..(pos+item_len)];
values[elem] = u8::from_str_radix(item, 16)
.map_err(|_| ParseColorError)?;
if short {
// Duplicate number for shorthand notation, i.e. `a` -> `aa`
values[elem] += values[elem] * 16;
}
}
Ok(Self::new(values[0], values[1], values[2], values[3]))
}
}
impl Debug for RgbaColor {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if f.alternate() {
write!(
f, "rgba({:02}, {:02}, {:02}, {:02})",
self.r, self.g, self.b, self.a,
)?;
} else {
write!(
f, "#{:02x}{:02x}{:02x}{:02x}",
self.r, self.g, self.b, self.a,
)?;
}
if self.healed {
f.write_str(" [healed]")?;
}
Ok(())
}
}
/// The error when parsing an `RgbaColor` fails.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct ParseColorError;
impl std::error::Error for ParseColorError {}
impl fmt::Display for ParseColorError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad("invalid color")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn 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)),
);
}
test("f61243ff", 0xf6, 0x12, 0x43, 0xff);
test("b3d8b3", 0xb3, 0xd8, 0xb3, 0xff);
test("fCd2a9AD", 0xfc, 0xd2, 0xa9, 0xad);
test("233", 0x22, 0x33, 0x33, 0xff);
test("111b", 0x11, 0x11, 0x11, 0xbb);
}
}

5
src/compute/mod.rs Normal file
View File

@ -0,0 +1,5 @@
//! Building blocks for the computational part.
pub mod scope;
pub mod table;
pub mod value;

50
src/compute/scope.rs Normal file
View File

@ -0,0 +1,50 @@
//! Mapping from identifiers to functions.
use std::collections::HashMap;
use std::fmt::{self, Debug, Formatter};
use super::value::FuncValue;
/// A map from identifiers to functions.
pub struct Scope {
functions: HashMap<String, FuncValue>,
fallback: FuncValue,
}
impl Scope {
// Create a new empty scope with a fallback function that is invoked when no
// match is found.
pub fn new(fallback: FuncValue) -> Self {
Self {
functions: HashMap::new(),
fallback,
}
}
/// Associate the given name with the function.
pub fn insert(&mut self, name: impl Into<String>, function: FuncValue) {
self.functions.insert(name.into(), function);
}
/// Return the function with the given name if there is one.
pub fn func(&self, name: &str) -> Option<&FuncValue> {
self.functions.get(name)
}
/// Return the function with the given name or the fallback if there is no
/// such function.
pub fn func_or_fallback(&self, name: &str) -> &FuncValue {
self.func(name).unwrap_or_else(|| self.fallback())
}
/// Return the fallback function.
pub fn fallback(&self) -> &FuncValue {
&self.fallback
}
}
impl Debug for Scope {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_set().entries(self.functions.keys()).finish()
}
}

View File

@ -1,17 +1,17 @@
//! A table data structure.
//! A key-value map that can also model array-like structures.
use std::collections::BTreeMap;
use std::fmt::{self, Debug, Formatter};
use std::ops::Index;
/// A table is a key-value map that can also model array-like structures.
use crate::syntax::span::{Span, Spanned};
/// A table data structure, which maps from integers (`u64`) or strings to a
/// generic value type.
///
/// An array-like table assigns value to successive indices from `0..n`. The
/// table type offers special support for this pattern through the `push`
/// method.
///
/// The keys of a table may be strings or integers (`u64`). The table is generic
/// over the value type.
/// The table can be used to model arrays by assigns values to successive
/// indices from `0..n`. The table type offers special support for this pattern
/// through the `push` method.
#[derive(Clone)]
pub struct Table<V> {
nums: BTreeMap<u64, V>,
@ -224,8 +224,8 @@ impl From<String> for OwnedKey {
}
}
impl From<&str> for OwnedKey {
fn from(string: &str) -> Self {
impl From<&'static str> for OwnedKey {
fn from(string: &'static str) -> Self {
Self::Str(string.to_string())
}
}
@ -255,6 +255,46 @@ impl<'a> From<&'a str> for BorrowedKey<'a> {
}
}
/// An table entry which tracks key and value span.
#[derive(Clone, PartialEq)]
pub struct SpannedEntry<V> {
pub key: Span,
pub val: Spanned<V>,
}
impl<V> SpannedEntry<V> {
/// Create a new entry.
pub fn new(key: Span, val: Spanned<V>) -> Self {
Self { key, val }
}
/// Create an entry with the same span for key and value.
pub fn val(val: Spanned<V>) -> Self {
Self { key: Span::ZERO, val }
}
/// Convert from `&SpannedEntry<T>` to `SpannedEntry<&T>`
pub fn as_ref(&self) -> SpannedEntry<&V> {
SpannedEntry { key: self.key, val: self.val.as_ref() }
}
/// Map the entry to a different value type.
pub fn map<U>(self, f: impl FnOnce(V) -> U) -> SpannedEntry<U> {
SpannedEntry { key: self.key, val: self.val.map(f) }
}
}
impl<V: Debug> Debug for SpannedEntry<V> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if f.alternate() {
f.write_str("key")?;
self.key.fmt(f)?;
f.write_str(" ")?;
}
self.val.fmt(f)
}
}
#[cfg(test)]
mod tests {
use super::Table;

474
src/compute/value.rs Normal file
View File

@ -0,0 +1,474 @@
//! Computational values: Syntactical expressions can be evaluated into these.
use std::fmt::{self, Debug, Formatter};
use std::rc::Rc;
use fontdock::{FontStyle, FontWeight, FontWidth};
use crate::color::RgbaColor;
use crate::layout::{Commands, Dir, LayoutContext, SpecAlign};
use crate::length::{Length, ScaleLength};
use crate::paper::Paper;
use crate::syntax::span::{Span, Spanned};
use crate::syntax::tree::SyntaxTree;
use crate::syntax::Ident;
use crate::{DynFuture, Feedback, Pass};
use super::table::{BorrowedKey, SpannedEntry, Table};
/// A computational value.
#[derive(Clone)]
pub enum Value {
/// An identifier: `ident`.
Ident(Ident),
/// A string: `"string"`.
Str(String),
/// A boolean: `true, false`.
Bool(bool),
/// A number: `1.2, 200%`.
Number(f64),
/// A length: `2cm, 5.2in`.
Length(Length),
/// A color value with alpha channel: `#f79143ff`.
Color(RgbaColor),
/// A table value: `(false, 12cm, greeting="hi")`.
Table(TableValue),
/// A syntax tree containing typesetting content.
Tree(SyntaxTree),
/// An executable function.
Func(FuncValue),
/// Layouting commands.
Commands(Commands),
}
impl Value {
/// A natural-language name of the type of this expression, e.g.
/// "identifier".
pub fn name(&self) -> &'static str {
use Value::*;
match self {
Ident(_) => "identifier",
Str(_) => "string",
Bool(_) => "bool",
Number(_) => "number",
Length(_) => "length",
Color(_) => "color",
Table(_) => "table",
Tree(_) => "syntax tree",
Func(_) => "function",
Commands(_) => "commands",
}
}
}
impl Debug for Value {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
use Value::*;
match self {
Ident(i) => i.fmt(f),
Str(s) => s.fmt(f),
Bool(b) => b.fmt(f),
Number(n) => n.fmt(f),
Length(s) => s.fmt(f),
Color(c) => c.fmt(f),
Table(t) => t.fmt(f),
Tree(t) => t.fmt(f),
Func(_) => f.pad("<function>"),
Commands(c) => c.fmt(f),
}
}
}
impl PartialEq for Value {
fn eq(&self, other: &Self) -> bool {
use Value::*;
match (self, other) {
(Ident(a), Ident(b)) => a == b,
(Str(a), Str(b)) => a == b,
(Bool(a), Bool(b)) => a == b,
(Number(a), Number(b)) => a == b,
(Length(a), Length(b)) => a == b,
(Color(a), Color(b)) => a == b,
(Table(a), Table(b)) => a == b,
(Tree(a), Tree(b)) => a == b,
(Func(a), Func(b)) => {
a.as_ref() as *const _ == b.as_ref() as *const _
}
(Commands(a), Commands(b)) => a == b,
_ => false,
}
}
}
/// An executable function value.
///
/// The first argument is a table containing the arguments passed to the
/// function. The function may be asynchronous (as such it returns a dynamic
/// future) and it may emit diagnostics, which are contained in the returned
/// `Pass`. In the end, the function must evaluate to `Value`. Your typical
/// typesetting function will return a `Commands` value which will instruct the
/// layouting engine to do what the function pleases.
///
/// The dynamic function object is wrapped in an `Rc` to keep `Value` clonable.
pub type FuncValue = Rc<
dyn Fn(TableValue, LayoutContext<'_>) -> DynFuture<Pass<Value>>
>;
/// A table of values.
///
/// # Example
/// ```typst
/// (false, 12cm, greeting="hi")
/// ```
pub type TableValue = Table<SpannedEntry<Value>>;
impl TableValue {
/// Retrieve and remove the matching value with the lowest number key,
/// skipping and ignoring all non-matching entries with lower keys.
pub fn take<T: TryFromValue>(&mut self) -> Option<T> {
for (&key, entry) in self.nums() {
let expr = entry.val.as_ref();
if let Some(val) = T::try_from_value(expr, &mut Feedback::new()) {
self.remove(key);
return Some(val);
}
}
None
}
/// Retrieve and remove the matching value with the lowest number key,
/// removing and generating errors for all non-matching entries with lower
/// keys.
pub fn expect<T: TryFromValue>(&mut self, f: &mut Feedback) -> Option<T> {
while let Some((num, _)) = self.first() {
let entry = self.remove(num).unwrap();
if let Some(val) = T::try_from_value(entry.val.as_ref(), f) {
return Some(val);
}
}
None
}
/// Retrieve and remove a matching value associated with the given key if
/// there is any.
///
/// Generates an error if the key exists but the value does not match.
pub fn take_with_key<'a, K, T>(&mut self, key: K, f: &mut Feedback) -> Option<T>
where
K: Into<BorrowedKey<'a>>,
T: TryFromValue,
{
self.remove(key).and_then(|entry| {
let expr = entry.val.as_ref();
T::try_from_value(expr, f)
})
}
/// Retrieve and remove all matching pairs with number keys, skipping and
/// ignoring non-matching entries.
///
/// The pairs are returned in order of increasing keys.
pub fn take_all_num<'a, T>(&'a mut self) -> impl Iterator<Item = (u64, T)> + 'a
where
T: TryFromValue,
{
let mut skip = 0;
std::iter::from_fn(move || {
for (&key, entry) in self.nums().skip(skip) {
let expr = entry.val.as_ref();
if let Some(val) = T::try_from_value(expr, &mut Feedback::new()) {
self.remove(key);
return Some((key, val));
}
skip += 1;
}
None
})
}
/// Retrieve and remove all matching values with number keys, skipping and
/// ignoring non-matching entries.
///
/// The values are returned in order of increasing keys.
pub fn take_all_num_vals<'a, T: 'a>(&'a mut self) -> impl Iterator<Item = T> + 'a
where
T: TryFromValue,
{
self.take_all_num::<T>().map(|(_, v)| v)
}
/// Retrieve and remove all matching pairs with string keys, skipping and
/// ignoring non-matching entries.
///
/// The pairs are returned in order of increasing keys.
pub fn take_all_str<'a, T>(&'a mut self) -> impl Iterator<Item = (String, T)> + 'a
where
T: TryFromValue,
{
let mut skip = 0;
std::iter::from_fn(move || {
for (key, entry) in self.strs().skip(skip) {
let expr = entry.val.as_ref();
if let Some(val) = T::try_from_value(expr, &mut Feedback::new()) {
let key = key.clone();
self.remove(&key);
return Some((key, val));
}
skip += 1;
}
None
})
}
/// Generated `"unexpected argument"` errors for all remaining entries.
pub fn unexpected(&self, f: &mut Feedback) {
for entry in self.values() {
let span = Span::merge(entry.key, entry.val.span);
error!(@f, span, "unexpected argument");
}
}
}
/// A trait for converting values into more specific types.
pub trait TryFromValue: Sized {
// This trait takes references because we don't want to move the value
// out of its origin in case this returns `None`. This solution is not
// perfect because we need to do some cloning in the impls for this trait,
// but we haven't got a better solution, for now.
/// Try to convert a value to this type.
///
/// Returns `None` and generates an appropriate error if the value is not
/// valid for this type.
fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option<Self>;
}
macro_rules! impl_match {
($type:ty, $name:expr, $($p:pat => $r:expr),* $(,)?) => {
impl TryFromValue for $type {
fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option<Self> {
#[allow(unreachable_patterns)]
match value.v {
$($p => Some($r)),*,
other => {
error!(
@f, value.span,
"expected {}, found {}", $name, other.name()
);
None
}
}
}
}
};
}
macro_rules! impl_ident {
($type:ty, $name:expr, $parse:expr) => {
impl TryFromValue for $type {
fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option<Self> {
if let Value::Ident(ident) = value.v {
let val = $parse(ident.as_str());
if val.is_none() {
error!(@f, value.span, "invalid {}", $name);
}
val
} else {
error!(
@f, value.span,
"expected {}, found {}", $name, value.v.name()
);
None
}
}
}
};
}
impl<T: TryFromValue> TryFromValue for Spanned<T> {
fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option<Self> {
let span = value.span;
T::try_from_value(value, f).map(|v| Spanned { v, span })
}
}
impl_match!(Value, "value", v => v.clone());
impl_match!(Ident, "identifier", Value::Ident(i) => i.clone());
impl_match!(String, "string", Value::Str(s) => s.clone());
impl_match!(bool, "bool", &Value::Bool(b) => b);
impl_match!(f64, "number", &Value::Number(n) => n);
impl_match!(Length, "length", &Value::Length(l) => l);
impl_match!(SyntaxTree, "tree", Value::Tree(t) => t.clone());
impl_match!(TableValue, "table", Value::Table(t) => t.clone());
impl_match!(FuncValue, "function", Value::Func(f) => f.clone());
impl_match!(ScaleLength, "number or length",
&Value::Length(length) => ScaleLength::Absolute(length),
&Value::Number(scale) => ScaleLength::Scaled(scale),
);
/// A value type that matches identifiers and strings and implements
/// `Into<String>`.
pub struct StringLike(pub String);
impl From<StringLike> for String {
fn from(like: StringLike) -> String {
like.0
}
}
impl_match!(StringLike, "identifier or string",
Value::Ident(Ident(s)) => StringLike(s.clone()),
Value::Str(s) => StringLike(s.clone()),
);
impl_ident!(Dir, "direction", |s| match s {
"ltr" => Some(Self::LTR),
"rtl" => Some(Self::RTL),
"ttb" => Some(Self::TTB),
"btt" => Some(Self::BTT),
_ => None,
});
impl_ident!(SpecAlign, "alignment", |s| match s {
"left" => Some(Self::Left),
"right" => Some(Self::Right),
"top" => Some(Self::Top),
"bottom" => Some(Self::Bottom),
"center" => Some(Self::Center),
_ => None,
});
impl_ident!(FontStyle, "font style", FontStyle::from_name);
impl_ident!(Paper, "paper", Paper::from_name);
impl TryFromValue for FontWeight {
fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option<Self> {
match value.v {
&Value::Number(weight) => {
const MIN: u16 = 100;
const MAX: u16 = 900;
Some(Self(if weight < MIN as f64 {
error!(@f, value.span, "the minimum font weight is {}", MIN);
MIN
} else if weight > MAX as f64 {
error!(@f, value.span, "the maximum font weight is {}", MAX);
MAX
} else {
weight.round() as u16
}))
}
Value::Ident(ident) => {
let weight = Self::from_name(ident.as_str());
if weight.is_none() {
error!(@f, value.span, "invalid font weight");
}
weight
}
other => {
error!(
@f, value.span,
"expected font weight (name or number), found {}",
other.name(),
);
None
}
}
}
}
impl TryFromValue for FontWidth {
fn try_from_value(value: Spanned<&Value>, f: &mut Feedback) -> Option<Self> {
match value.v {
&Value::Number(width) => {
const MIN: u16 = 1;
const MAX: u16 = 9;
Self::new(if width < MIN as f64 {
error!(@f, value.span, "the minimum font width is {}", MIN);
MIN
} else if width > MAX as f64 {
error!(@f, value.span, "the maximum font width is {}", MAX);
MAX
} else {
width.round() as u16
})
}
Value::Ident(ident) => {
let width = Self::from_name(ident.as_str());
if width.is_none() {
error!(@f, value.span, "invalid font width");
}
width
}
other => {
error!(
@f, value.span,
"expected font width (name or number), found {}",
other.name(),
);
None
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(value: Value) -> SpannedEntry<Value> {
SpannedEntry::val(Spanned::zero(value))
}
#[test]
fn test_table_take_removes_correct_entry() {
let mut table = Table::new();
table.insert(1, entry(Value::Bool(false)));
table.insert(2, entry(Value::Str("hi".to_string())));
assert_eq!(table.take::<String>(), Some("hi".to_string()));
assert_eq!(table.len(), 1);
assert_eq!(table.take::<bool>(), Some(false));
assert!(table.is_empty());
}
#[test]
fn test_table_expect_errors_about_previous_entries() {
let mut f = Feedback::new();
let mut table = Table::new();
table.insert(1, entry(Value::Bool(false)));
table.insert(3, entry(Value::Str("hi".to_string())));
table.insert(5, entry(Value::Bool(true)));
assert_eq!(table.expect::<String>(&mut f), Some("hi".to_string()));
assert_eq!(f.diagnostics, [error!(Span::ZERO, "expected string, found bool")]);
assert_eq!(table.len(), 1);
}
#[test]
fn test_table_take_with_key_removes_the_entry() {
let mut f = Feedback::new();
let mut table = Table::new();
table.insert(1, entry(Value::Bool(false)));
table.insert("hi", entry(Value::Bool(true)));
assert_eq!(table.take_with_key::<_, bool>(1, &mut f), Some(false));
assert_eq!(table.take_with_key::<_, f64>("hi", &mut f), None);
assert_eq!(f.diagnostics, [error!(Span::ZERO, "expected number, found bool")]);
assert!(table.is_empty());
}
#[test]
fn test_table_take_all_removes_the_correct_entries() {
let mut table = Table::new();
table.insert(1, entry(Value::Bool(false)));
table.insert(3, entry(Value::Number(0.0)));
table.insert(7, entry(Value::Bool(true)));
assert_eq!(
table.take_all_num::<bool>().collect::<Vec<_>>(),
[(1, false), (7, true)],
);
assert_eq!(table.len(), 1);
assert_eq!(table[3].val.v, Value::Number(0.0));
}
}

View File

@ -13,7 +13,7 @@ use crate::syntax::span::SpanVec;
pub type Diagnostics = SpanVec<Diagnostic>;
/// A diagnostic that arose in parsing or layouting.
#[derive(Debug, Clone, Eq, PartialEq)]
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
#[cfg_attr(feature = "serialize", derive(Serialize))]
pub struct Diagnostic {
/// How severe / important the diagnostic is.

View File

@ -10,9 +10,7 @@ mod tree;
/// Basic types used across the layouting engine.
pub mod prelude {
pub use super::primitive::*;
pub use super::{
BoxLayout, layout, Layout, LayoutContext, LayoutSpace, MultiLayout,
};
pub use super::{BoxLayout, layout, LayoutContext, LayoutSpace, MultiLayout};
pub use Dir::*;
pub use GenAlign::*;
pub use GenAxis::*;
@ -23,13 +21,11 @@ pub mod prelude {
pub use primitive::*;
pub use tree::layout_tree as layout;
use async_trait::async_trait;
use crate::compute::scope::Scope;
use crate::font::SharedFontLoader;
use crate::geom::{Margins, Size};
use crate::style::{LayoutStyle, PageStyle, TextStyle};
use crate::syntax::tree::SyntaxTree;
use crate::Pass;
use elements::LayoutElements;
use prelude::*;
@ -48,18 +44,13 @@ pub struct BoxLayout {
pub elements: LayoutElements,
}
/// Command-based layouting.
#[async_trait(?Send)]
pub trait Layout {
/// Create a sequence of layouting commands to execute.
async fn layout<'a>(&'a self, ctx: LayoutContext<'_>) -> Pass<Commands<'a>>;
}
/// The context for layouting.
#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct LayoutContext<'a> {
/// The font loader to query fonts from when typesetting text.
pub loader: &'a SharedFontLoader,
/// The function scope.
pub scope: &'a Scope,
/// The style for pages and text.
pub style: &'a LayoutStyle,
/// The unpadded size of this container (the base 100% for relative sizes).
@ -118,11 +109,11 @@ impl LayoutSpace {
}
/// A sequence of layouting commands.
pub type Commands<'a> = Vec<Command<'a>>;
pub type Commands = Vec<Command>;
/// Commands executable by the layouting engine.
#[derive(Debug, Clone)]
pub enum Command<'a> {
#[derive(Debug, Clone, PartialEq)]
pub enum Command {
/// Layout the given tree in the current context (i.e. not nested). The
/// content of the tree is not laid out into a separate box and then added,
/// but simply laid out flatly in the active layouting process.
@ -130,7 +121,7 @@ pub enum Command<'a> {
/// This has the effect that the content fits nicely into the active line
/// layouting, enabling functions to e.g. change the style of some piece of
/// text while keeping it part of the current paragraph.
LayoutSyntaxTree(&'a SyntaxTree),
LayoutSyntaxTree(SyntaxTree),
/// Add a finished layout.
Add(BoxLayout),

View File

@ -1,9 +1,10 @@
//! Layouting of syntax trees.
use crate::compute::value::Value;
use crate::style::LayoutStyle;
use crate::syntax::decoration::Decoration;
use crate::syntax::span::{Span, Spanned};
use crate::syntax::tree::{DynamicNode, SyntaxNode, SyntaxTree};
use crate::syntax::span::{Offset, Span, Spanned};
use crate::syntax::tree::{CallExpr, SyntaxNode, SyntaxTree};
use crate::{DynFuture, Feedback, Pass};
use super::line::{LineContext, LineLayouter};
use super::text::{layout_text, TextContext};
@ -66,60 +67,28 @@ impl<'a> TreeLayouter<'a> {
self.style.text.word_spacing(),
SpacingKind::WORD,
);
},
}
SyntaxNode::Linebreak => self.layouter.finish_line(),
SyntaxNode::ToggleItalic => {
self.style.text.italic = !self.style.text.italic;
decorate(self, Decoration::Italic);
}
SyntaxNode::ToggleBolder => {
self.style.text.bolder = !self.style.text.bolder;
decorate(self, Decoration::Bold);
}
SyntaxNode::Text(text) => {
if self.style.text.italic {
decorate(self, Decoration::Italic);
}
if self.style.text.bolder {
decorate(self, Decoration::Bold);
}
if self.style.text.italic { decorate(self, Decoration::Italic); }
if self.style.text.bolder { decorate(self, Decoration::Bold); }
self.layout_text(text).await;
}
SyntaxNode::Raw(lines) => {
// TODO: Make this more efficient.
let fallback = self.style.text.fallback.clone();
self.style.text.fallback
.list_mut()
.insert(0, "monospace".to_string());
self.style.text.fallback.flatten();
// Layout the first line.
let mut iter = lines.iter();
if let Some(line) = iter.next() {
self.layout_text(line).await;
}
// Put a newline before each following line.
for line in iter {
self.layouter.finish_line();
self.layout_text(line).await;
}
self.style.text.fallback = fallback;
}
SyntaxNode::Raw(lines) => self.layout_raw(lines).await,
SyntaxNode::Par(par) => self.layout_par(par).await,
SyntaxNode::Dyn(dynamic) => {
self.layout_dyn(Spanned::new(dynamic.as_ref(), node.span)).await;
SyntaxNode::Call(call) => {
self.layout_call(Spanned::new(call, node.span)).await;
}
}
}
@ -133,19 +102,35 @@ impl<'a> TreeLayouter<'a> {
self.layout_tree(par).await;
}
async fn layout_dyn(&mut self, dynamic: Spanned<&dyn DynamicNode>) {
// Execute the dynamic node's command-generating layout function.
let layouted = dynamic.v.layout(LayoutContext {
async fn layout_call(&mut self, call: Spanned<&CallExpr>) {
let name = call.v.name.v.as_str();
let span = call.v.name.span.offset(call.span.start);
let (func, deco) = if let Some(func) = self.ctx.scope.func(name) {
(func, Decoration::Resolved)
} else {
error!(@self.feedback, span, "unknown function");
(self.ctx.scope.fallback(), Decoration::Unresolved)
};
self.feedback.decorations.push(Spanned::new(deco, span));
let args = call.v.args.eval();
let pass = func(args, LayoutContext {
style: &self.style,
spaces: self.layouter.remaining(),
root: true,
..self.ctx
}).await;
self.feedback.extend_offset(layouted.feedback, dynamic.span.start);
self.feedback.extend_offset(pass.feedback, call.span.start);
for command in layouted.output {
self.execute_command(command, dynamic.span).await;
if let Value::Commands(commands) = pass.output {
for command in commands {
self.execute_command(command, call.span).await;
}
} else {
self.layout_raw(&[format!("{:?}", pass.output)]).await;
}
}
@ -163,11 +148,35 @@ impl<'a> TreeLayouter<'a> {
);
}
async fn execute_command(&mut self, command: Command<'_>, span: Span) {
async fn layout_raw(&mut self, lines: &[String]) {
// TODO: Make this more efficient.
let fallback = self.style.text.fallback.clone();
self.style.text.fallback
.list_mut()
.insert(0, "monospace".to_string());
self.style.text.fallback.flatten();
// Layout the first line.
let mut iter = lines.iter();
if let Some(line) = iter.next() {
self.layout_text(line).await;
}
// Put a newline before each following line.
for line in iter {
self.layouter.finish_line();
self.layout_text(line).await;
}
self.style.text.fallback = fallback;
}
async fn execute_command(&mut self, command: Command, span: Span) {
use Command::*;
match command {
LayoutSyntaxTree(tree) => self.layout_tree(tree).await,
LayoutSyntaxTree(tree) => self.layout_tree(&tree).await,
Add(layout) => self.layouter.add(layout),
AddMultiple(layouts) => self.layouter.add_multiple(layouts),

View File

@ -24,9 +24,9 @@
mod macros;
#[macro_use]
pub mod diagnostic;
#[macro_use]
pub mod func;
pub mod color;
pub mod compute;
pub mod export;
pub mod font;
pub mod geom;
@ -34,22 +34,24 @@ pub mod layout;
pub mod length;
pub mod library;
pub mod paper;
pub mod prelude;
pub mod style;
pub mod syntax;
pub mod table;
use std::fmt::Debug;
use std::future::Future;
use std::pin::Pin;
use crate::compute::scope::Scope;
use crate::compute::value::Value;
use crate::diagnostic::Diagnostics;
use crate::font::SharedFontLoader;
use crate::layout::MultiLayout;
use crate::layout::{Commands, MultiLayout};
use crate::style::{LayoutStyle, PageStyle, TextStyle};
use crate::syntax::decoration::Decorations;
use crate::syntax::parsing::{parse, ParseState};
use crate::syntax::parsing::parse;
use crate::syntax::span::{Offset, Pos};
use crate::syntax::tree::{DynamicNode, SyntaxNode, SyntaxTree};
use crate::syntax::tree::SyntaxTree;
/// Transforms source code into typesetted layouts.
///
@ -57,10 +59,10 @@ use crate::syntax::tree::{DynamicNode, SyntaxNode, SyntaxTree};
pub struct Typesetter {
/// The font loader shared by all typesetting processes.
loader: SharedFontLoader,
/// A scope that contains the standard library function definitions.
std: Scope,
/// The base layouting style.
style: LayoutStyle,
/// The base parser state.
parse_state: ParseState,
}
impl Typesetter {
@ -68,8 +70,8 @@ impl Typesetter {
pub fn new(loader: SharedFontLoader) -> Self {
Self {
loader,
std: crate::library::_std(),
style: LayoutStyle::default(),
parse_state: ParseState { scope: crate::library::_std() },
}
}
@ -85,7 +87,7 @@ impl Typesetter {
/// Parse source code into a syntax tree.
pub fn parse(&self, src: &str) -> Pass<SyntaxTree> {
parse(src, Pos::ZERO, &self.parse_state)
parse(src, Pos::ZERO)
}
/// Layout a syntax tree and return the produced layout.
@ -97,6 +99,7 @@ impl Typesetter {
&tree,
LayoutContext {
loader: &self.loader,
scope: &self.std,
style: &self.style,
base: self.style.page.size.unpadded(margins),
spaces: vec![LayoutSpace {
@ -155,10 +158,10 @@ impl<T> Pass<T> {
}
}
impl Pass<SyntaxNode> {
/// Create a new pass from an unboxed dynamic node and feedback data..
pub fn node<T: DynamicNode + 'static>(node: T, feedback: Feedback) -> Self {
Pass::new(SyntaxNode::boxed(node), feedback)
impl Pass<Value> {
/// Create a new pass with a list of layouting commands.
pub fn commands(commands: Commands, feedback: Feedback) -> Self {
Pass::new(Value::Commands(commands), feedback)
}
}

View File

@ -10,67 +10,49 @@ use super::*;
/// - `vertical`: Any of `top`, `bottom` or `center`.
///
/// There may not be two alignment specifications for the same axis.
pub fn align(call: FuncCall, _: &ParseState) -> Pass<SyntaxNode> {
pub async fn align(mut args: TableValue, mut ctx: LayoutContext<'_>) -> Pass<Value> {
let mut f = Feedback::new();
let mut args = call.args;
let node = AlignNode {
content: args.take::<SyntaxTree>(),
aligns: args.take_all_num_vals::<Spanned<SpecAlign>>().collect(),
h: args.take_with_key::<_, Spanned<SpecAlign>>("horizontal", &mut f),
v: args.take_with_key::<_, Spanned<SpecAlign>>("vertical", &mut f),
};
let content = args.take::<SyntaxTree>();
let aligns: Vec<_> = args.take_all_num_vals::<Spanned<SpecAlign>>().collect();
let h = args.take_with_key::<_, Spanned<SpecAlign>>("horizontal", &mut f);
let v = args.take_with_key::<_, Spanned<SpecAlign>>("vertical", &mut f);
args.unexpected(&mut f);
Pass::node(node, f)
}
#[derive(Debug, Clone, PartialEq)]
struct AlignNode {
content: Option<SyntaxTree>,
aligns: SpanVec<SpecAlign>,
h: Option<Spanned<SpecAlign>>,
v: Option<Spanned<SpecAlign>>,
}
ctx.base = ctx.spaces[0].size;
#[async_trait(?Send)]
impl Layout for AlignNode {
async fn layout<'a>(&'a self, mut ctx: LayoutContext<'_>) -> Pass<Commands<'a>> {
let mut f = Feedback::new();
let axes = ctx.axes;
let all = aligns.iter()
.map(|align| {
let spec = align.v.axis().unwrap_or(axes.primary.axis());
(spec, align)
})
.chain(h.iter().map(|align| (Horizontal, align)))
.chain(v.iter().map(|align| (Vertical, align)));
ctx.base = ctx.spaces[0].size;
let axes = ctx.axes;
let all = self.aligns.iter()
.map(|align| {
let spec = align.v.axis().unwrap_or(axes.primary.axis());
(spec, align)
})
.chain(self.h.iter().map(|align| (Horizontal, align)))
.chain(self.v.iter().map(|align| (Vertical, align)));
let mut had = [false; 2];
for (axis, align) in all {
if align.v.axis().map(|a| a != axis).unwrap_or(false) {
error!(
@f, align.span,
"invalid alignment {} for {} axis", align.v, axis,
);
} else if had[axis as usize] {
error!(@f, align.span, "duplicate alignment for {} axis", axis);
} else {
had[axis as usize] = true;
let gen_axis = axis.to_generic(ctx.axes);
let gen_align = align.v.to_generic(ctx.axes);
*ctx.align.get_mut(gen_axis) = gen_align;
}
let mut had = [false; 2];
for (axis, align) in all {
if align.v.axis().map(|a| a != axis).unwrap_or(false) {
error!(
@f, align.span,
"invalid alignment {} for {} axis", align.v, axis,
);
} else if had[axis as usize] {
error!(@f, align.span, "duplicate alignment for {} axis", axis);
} else {
had[axis as usize] = true;
let gen_axis = axis.to_generic(ctx.axes);
let gen_align = align.v.to_generic(ctx.axes);
*ctx.align.get_mut(gen_axis) = gen_align;
}
Pass::new(match &self.content {
Some(tree) => {
let layouted = layout(tree, ctx).await;
f.extend(layouted.feedback);
vec![AddMultiple(layouted.output)]
}
None => vec![SetAlignment(ctx.align)],
}, f)
}
Pass::commands(match content {
Some(tree) => {
let layouted = layout(&tree, ctx).await;
f.extend(layouted.feedback);
vec![AddMultiple(layouted.output)]
}
None => vec![SetAlignment(ctx.align)],
}, f)
}

View File

@ -6,48 +6,32 @@ use super::*;
/// # Keyword arguments
/// - `width`: The width of the box (length of relative to parent's width).
/// - `height`: The height of the box (length of relative to parent's height).
pub fn boxed(call: FuncCall, _: &ParseState) -> Pass<SyntaxNode> {
pub async fn boxed(mut args: TableValue, mut ctx: LayoutContext<'_>) -> Pass<Value> {
let mut f = Feedback::new();
let mut args = call.args;
let node = BoxNode {
content: args.take::<SyntaxTree>().unwrap_or(SyntaxTree::new()),
width: args.take_with_key::<_, ScaleLength>("width", &mut f),
height: args.take_with_key::<_, ScaleLength>("height", &mut f),
};
let content = args.take::<SyntaxTree>().unwrap_or(SyntaxTree::new());
let width = args.take_with_key::<_, ScaleLength>("width", &mut f);
let height = args.take_with_key::<_, ScaleLength>("height", &mut f);
args.unexpected(&mut f);
Pass::node(node, f)
}
#[derive(Debug, Clone, PartialEq)]
struct BoxNode {
content: SyntaxTree,
width: Option<ScaleLength>,
height: Option<ScaleLength>,
}
#[async_trait(?Send)]
impl Layout for BoxNode {
async fn layout<'a>(&'a self, mut ctx: LayoutContext<'_>) -> Pass<Commands<'a>> {
ctx.spaces.truncate(1);
ctx.repeat = false;
self.width.with(|v| {
let length = v.raw_scaled(ctx.base.x);
ctx.base.x = length;
ctx.spaces[0].size.x = length;
ctx.spaces[0].expansion.horizontal = true;
});
self.height.with(|v| {
let length = v.raw_scaled(ctx.base.y);
ctx.base.y = length;
ctx.spaces[0].size.y = length;
ctx.spaces[0].expansion.vertical = true;
});
layout(&self.content, ctx).await.map(|out| {
let layout = out.into_iter().next().unwrap();
vec![Add(layout)]
})
}
ctx.spaces.truncate(1);
ctx.repeat = false;
width.with(|v| {
let length = v.raw_scaled(ctx.base.x);
ctx.base.x = length;
ctx.spaces[0].size.x = length;
ctx.spaces[0].expansion.horizontal = true;
});
height.with(|v| {
let length = v.raw_scaled(ctx.base.y);
ctx.base.y = length;
ctx.spaces[0].size.y = length;
ctx.spaces[0].expansion.vertical = true;
});
let layouted = layout(&content, ctx).await;
let layout = layouted.output.into_iter().next().unwrap();
f.extend(layouted.feedback);
Pass::commands(vec![Add(layout)], f)
}

View File

@ -18,80 +18,60 @@ use super::*;
/// ```typst
/// serif = ("Source Serif Pro", "Noto Serif")
/// ```
pub fn font(call: FuncCall, _: &ParseState) -> Pass<SyntaxNode> {
pub async fn font(mut args: TableValue, ctx: LayoutContext<'_>) -> Pass<Value> {
let mut f = Feedback::new();
let mut args = call.args;
let node = FontNode {
content: args.take::<SyntaxTree>(),
size: args.take::<ScaleLength>(),
style: args.take_with_key::<_, FontStyle>("style", &mut f),
weight: args.take_with_key::<_, FontWeight>("weight", &mut f),
width: args.take_with_key::<_, FontWidth>("width", &mut f),
list: args.take_all_num_vals::<StringLike>()
.map(|s| s.0.to_lowercase())
.collect(),
classes: args.take_all_str::<TableExpr>()
.map(|(class, mut table)| {
let fallback = table.take_all_num_vals::<StringLike>()
.map(|s| s.0.to_lowercase())
.collect();
(class, fallback)
})
.collect()
};
let content = args.take::<SyntaxTree>();
let size = args.take::<ScaleLength>();
let style = args.take_with_key::<_, FontStyle>("style", &mut f);
let weight = args.take_with_key::<_, FontWeight>("weight", &mut f);
let width = args.take_with_key::<_, FontWidth>("width", &mut f);
let list: Vec<_> = args.take_all_num_vals::<StringLike>()
.map(|s| s.0.to_lowercase())
.collect();
let classes: Vec<(_, Vec<_>)> = args.take_all_str::<TableValue>()
.map(|(class, mut table)| {
let fallback = table.take_all_num_vals::<StringLike>()
.map(|s| s.0.to_lowercase())
.collect();
(class, fallback)
})
.collect();
args.unexpected(&mut f);
Pass::node(node, f)
}
#[derive(Debug, Clone, PartialEq)]
struct FontNode {
content: Option<SyntaxTree>,
size: Option<ScaleLength>,
style: Option<FontStyle>,
weight: Option<FontWeight>,
width: Option<FontWidth>,
list: Vec<String>,
classes: Vec<(String, Vec<String>)>,
}
let mut text = ctx.style.text.clone();
#[async_trait(?Send)]
impl Layout for FontNode {
async fn layout<'a>(&'a self, ctx: LayoutContext<'_>) -> Pass<Commands<'a>> {
let mut text = ctx.style.text.clone();
self.size.with(|s| match s {
ScaleLength::Absolute(length) => {
text.base_font_size = length.as_raw();
text.font_scale = 1.0;
}
ScaleLength::Scaled(scale) => text.font_scale = scale,
});
self.style.with(|s| text.variant.style = s);
self.weight.with(|w| text.variant.weight = w);
self.width.with(|w| text.variant.width = w);
if !self.list.is_empty() {
*text.fallback.list_mut() = self.list.iter()
.map(|s| s.to_lowercase())
.collect();
size.with(|s| match s {
ScaleLength::Absolute(length) => {
text.base_font_size = length.as_raw();
text.font_scale = 1.0;
}
ScaleLength::Scaled(scale) => text.font_scale = scale,
});
for (class, fallback) in &self.classes {
text.fallback.set_class_list(class.clone(), fallback.clone());
}
style.with(|s| text.variant.style = s);
weight.with(|w| text.variant.weight = w);
width.with(|w| text.variant.width = w);
text.fallback.flatten();
Pass::okay(match &self.content {
Some(tree) => vec![
SetTextStyle(text),
LayoutSyntaxTree(tree),
SetTextStyle(ctx.style.text.clone()),
],
None => vec![SetTextStyle(text)],
})
if !list.is_empty() {
*text.fallback.list_mut() = list.iter()
.map(|s| s.to_lowercase())
.collect();
}
for (class, fallback) in classes {
text.fallback.set_class_list(class.clone(), fallback.clone());
}
text.fallback.flatten();
Pass::commands(match content {
Some(tree) => vec![
SetTextStyle(text),
LayoutSyntaxTree(tree),
SetTextStyle(ctx.style.text.clone()),
],
None => vec![SetTextStyle(text)],
}, f)
}

View File

@ -5,30 +5,60 @@ mod boxed;
mod font;
mod page;
mod spacing;
mod val;
pub use align::*;
pub use boxed::*;
pub use font::*;
pub use page::*;
pub use spacing::*;
pub use val::*;
use crate::func::prelude::*;
use crate::syntax::scope::Scope;
use std::rc::Rc;
/// Create a scope with all standard library functions.
pub fn _std() -> Scope {
let mut std = Scope::new(Box::new(val));
use crate::compute::scope::Scope;
use crate::prelude::*;
std.insert("val", Box::new(val));
std.insert("font", Box::new(font));
std.insert("page", Box::new(page));
std.insert("align", Box::new(align));
std.insert("box", Box::new(boxed));
std.insert("pagebreak", Box::new(pagebreak));
std.insert("h", Box::new(h));
std.insert("v", Box::new(v));
std
macro_rules! std {
(fallback: $fallback:expr $(, $name:literal => $func:expr)* $(,)?) => {
/// Create a scope with all standard library functions.
pub fn _std() -> Scope {
let mut std = Scope::new(wrap!(val));
$(std.insert($name, wrap!($func));)*
std
}
};
}
macro_rules! wrap {
($func:expr) => {
Rc::new(|args, ctx| Box::pin($func(args, ctx)))
};
}
std! {
fallback: val,
"align" => align,
"box" => boxed,
"dump" => dump,
"font" => font,
"h" => h,
"page" => page,
"pagebreak" => pagebreak,
"v" => v,
"val" => val,
}
/// `val`: Layouts its body flatly, ignoring other arguments.
///
/// This is also the fallback function, which is used when a function name
/// cannot be resolved.
pub async fn val(mut args: TableValue, _: LayoutContext<'_>) -> Pass<Value> {
Pass::commands(match args.take::<SyntaxTree>() {
Some(tree) => vec![LayoutSyntaxTree(tree)],
None => vec![],
}, Feedback::new())
}
/// `dump`: Dumps its arguments.
pub async fn dump(args: TableValue, _: LayoutContext<'_>) -> Pass<Value> {
Pass::okay(Value::Table(args))
}

View File

@ -16,78 +16,46 @@ use super::*;
/// - `top`: The top margin (length or relative to height).
/// - `bottom`: The bottom margin (length or relative to height).
/// - `flip`: Flips custom or paper-defined width and height (boolean).
pub fn page(call: FuncCall, _: &ParseState) -> Pass<SyntaxNode> {
pub async fn page(mut args: TableValue, ctx: LayoutContext<'_>) -> Pass<Value> {
let mut f = Feedback::new();
let mut args = call.args;
let node = PageNode {
paper: args.take::<Paper>(),
width: args.take_with_key::<_, Length>("width", &mut f),
height: args.take_with_key::<_, Length>("height", &mut f),
margins: args.take_with_key::<_, ScaleLength>("margins", &mut f),
left: args.take_with_key::<_, ScaleLength>("left", &mut f),
right: args.take_with_key::<_, ScaleLength>("right", &mut f),
top: args.take_with_key::<_, ScaleLength>("top", &mut f),
bottom: args.take_with_key::<_, ScaleLength>("bottom", &mut f),
flip: args.take_with_key::<_, bool>("flip", &mut f).unwrap_or(false),
};
let paper = args.take::<Paper>();
let width = args.take_with_key::<_, Length>("width", &mut f);
let height = args.take_with_key::<_, Length>("height", &mut f);
let margins = args.take_with_key::<_, ScaleLength>("margins", &mut f);
let left = args.take_with_key::<_, ScaleLength>("left", &mut f);
let right = args.take_with_key::<_, ScaleLength>("right", &mut f);
let top = args.take_with_key::<_, ScaleLength>("top", &mut f);
let bottom = args.take_with_key::<_, ScaleLength>("bottom", &mut f);
let flip = args.take_with_key::<_, bool>("flip", &mut f).unwrap_or(false);
args.unexpected(&mut f);
Pass::node(node, f)
}
#[derive(Debug, Clone, PartialEq)]
struct PageNode {
paper: Option<Paper>,
width: Option<Length>,
height: Option<Length>,
margins: Option<ScaleLength>,
left: Option<ScaleLength>,
right: Option<ScaleLength>,
top: Option<ScaleLength>,
bottom: Option<ScaleLength>,
flip: bool,
}
let mut style = ctx.style.page;
#[async_trait(?Send)]
impl Layout for PageNode {
async fn layout<'a>(&'a self, ctx: LayoutContext<'_>) -> Pass<Commands<'a>> {
let mut style = ctx.style.page;
if let Some(paper) = self.paper {
style.class = paper.class;
style.size = paper.size();
} else if self.width.is_some() || self.height.is_some() {
style.class = PaperClass::Custom;
}
self.width.with(|v| style.size.x = v.as_raw());
self.height.with(|v| style.size.y = v.as_raw());
self.margins.with(|v| style.margins.set_all(Some(v)));
self.left.with(|v| style.margins.left = Some(v));
self.right.with(|v| style.margins.right = Some(v));
self.top.with(|v| style.margins.top = Some(v));
self.bottom.with(|v| style.margins.bottom = Some(v));
if self.flip {
style.size.swap();
}
Pass::okay(vec![SetPageStyle(style)])
if let Some(paper) = paper {
style.class = paper.class;
style.size = paper.size();
} else if width.is_some() || height.is_some() {
style.class = PaperClass::Custom;
}
width.with(|v| style.size.x = v.as_raw());
height.with(|v| style.size.y = v.as_raw());
margins.with(|v| style.margins.set_all(Some(v)));
left.with(|v| style.margins.left = Some(v));
right.with(|v| style.margins.right = Some(v));
top.with(|v| style.margins.top = Some(v));
bottom.with(|v| style.margins.bottom = Some(v));
if flip {
style.size.swap();
}
Pass::commands(vec![SetPageStyle(style)], f)
}
/// `pagebreak`: Ends the current page.
pub fn pagebreak(call: FuncCall, _: &ParseState) -> Pass<SyntaxNode> {
pub async fn pagebreak(args: TableValue, _: LayoutContext<'_>) -> Pass<Value> {
let mut f = Feedback::new();
call.args.unexpected(&mut f);
Pass::node(PageBreakNode, f)
}
#[derive(Debug, Default, Clone, PartialEq)]
struct PageBreakNode;
#[async_trait(?Send)]
impl Layout for PageBreakNode {
async fn layout<'a>(&'a self, _: LayoutContext<'_>) -> Pass<Commands<'a>> {
Pass::okay(vec![BreakPage])
}
args.unexpected(&mut f);
Pass::commands(vec![BreakPage], f)
}

View File

@ -6,44 +6,32 @@ use super::*;
///
/// # Positional arguments
/// - The spacing (length or relative to font size).
pub fn h(call: FuncCall, _: &ParseState) -> Pass<SyntaxNode> {
spacing(call, Horizontal)
pub async fn h(args: TableValue, ctx: LayoutContext<'_>) -> Pass<Value> {
spacing(args, ctx, Horizontal).await
}
/// `v`: Add vertical spacing.
///
/// # Positional arguments
/// - The spacing (length or relative to font size).
pub fn v(call: FuncCall, _: &ParseState) -> Pass<SyntaxNode> {
spacing(call, Vertical)
pub async fn v(args: TableValue, ctx: LayoutContext<'_>) -> Pass<Value> {
spacing(args, ctx, Vertical).await
}
fn spacing(call: FuncCall, axis: SpecAxis) -> Pass<SyntaxNode> {
async fn spacing(
mut args: TableValue,
ctx: LayoutContext<'_>,
axis: SpecAxis,
) -> Pass<Value> {
let mut f = Feedback::new();
let mut args = call.args;
let node = SpacingNode {
spacing: args.expect::<ScaleLength>(&mut f)
.map(|s| (axis, s))
.or_missing(call.name.span, "spacing", &mut f),
};
let spacing = args.expect::<ScaleLength>(&mut f).map(|s| (axis, s));
args.unexpected(&mut f);
Pass::node(node, f)
}
#[derive(Debug, Clone, PartialEq)]
struct SpacingNode {
spacing: Option<(SpecAxis, ScaleLength)>,
}
#[async_trait(?Send)]
impl Layout for SpacingNode {
async fn layout<'a>(&'a self, ctx: LayoutContext<'_>) -> Pass<Commands<'a>> {
Pass::okay(if let Some((axis, spacing)) = self.spacing {
let axis = axis.to_generic(ctx.axes);
let spacing = spacing.raw_scaled(ctx.style.text.font_size());
vec![AddSpacing(spacing, SpacingKind::Hard, axis)]
} else {
vec![]
})
}
Pass::commands(if let Some((axis, spacing)) = spacing {
let axis = axis.to_generic(ctx.axes);
let spacing = spacing.raw_scaled(ctx.style.text.font_size());
vec![AddSpacing(spacing, SpacingKind::Hard, axis)]
} else {
vec![]
}, f)
}

View File

@ -1,28 +0,0 @@
use super::*;
/// `val`: Ignores all arguments and layouts its body flatly.
///
/// This is also the fallback function, which is used when a function name
/// cannot be resolved.
pub fn val(call: FuncCall, _: &ParseState) -> Pass<SyntaxNode> {
let mut args = call.args;
let node = ValNode {
content: args.take::<SyntaxTree>(),
};
Pass::node(node, Feedback::new())
}
#[derive(Debug, Clone, PartialEq)]
struct ValNode {
content: Option<SyntaxTree>,
}
#[async_trait(?Send)]
impl Layout for ValNode {
async fn layout<'a>(&'a self, _: LayoutContext<'_>) -> Pass<Commands<'a>> {
Pass::okay(match &self.content {
Some(tree) => vec![LayoutSyntaxTree(tree)],
None => vec![],
})
}
}

View File

@ -1,21 +1,15 @@
//! Tools for building custom functions.
//! A prelude for building custom functions.
/// Useful things for creating functions.
pub mod prelude {
pub use async_trait::async_trait;
pub use crate::layout::prelude::*;
pub use crate::layout::Commands;
pub use crate::layout::Command::{self, *};
pub use crate::style::*;
pub use crate::syntax::expr::*;
pub use crate::syntax::parsing::{parse, FuncCall, ParseState};
pub use crate::syntax::span::{Pos, Span, SpanVec, Spanned};
pub use crate::syntax::tree::{DynamicNode, SyntaxNode, SyntaxTree};
pub use crate::{Pass, Feedback};
pub use super::*;
}
use prelude::*;
pub use crate::compute::value::*;
pub use crate::layout::prelude::*;
pub use crate::layout::Commands;
pub use crate::layout::Command::{self, *};
pub use crate::style::*;
pub use crate::syntax::parsing::parse;
pub use crate::syntax::span::{Pos, Span, SpanVec, Spanned};
pub use crate::syntax::tree::*;
pub use crate::{Pass, Feedback};
pub use super::*;
/// Extra methods on `Option`s used for function argument parsing.
pub trait OptionExt<T>: Sized {

View File

@ -9,18 +9,18 @@ use super::span::SpanVec;
pub type Decorations = SpanVec<Decoration>;
/// Decorations for semantic syntax highlighting.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[cfg_attr(feature = "serialize", derive(Serialize))]
#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
pub enum Decoration {
/// A valid, successfully resolved function name.
ResolvedFunc,
/// An invalid, unresolved function name.
UnresolvedFunc,
/// The key part of a key-value entry in a table.
TableKey,
/// Text in italics.
Italic,
/// Text in bold.
Bold,
/// A valid, successfully resolved name.
Resolved,
/// An invalid, unresolved name.
Unresolved,
/// The key part of a key-value entry in a table.
TableKey,
}

View File

@ -1,639 +0,0 @@
//! Expressions in function headers.
use std::fmt::{self, Debug, Formatter};
use std::str::FromStr;
use std::u8;
use fontdock::{FontStyle, FontWeight, FontWidth};
use crate::layout::{Dir, SpecAlign};
use crate::length::{Length, ScaleLength};
use crate::paper::Paper;
use crate::table::{BorrowedKey, Table};
use crate::Feedback;
use super::parsing::FuncCall;
use super::span::{Span, Spanned};
use super::tokens::is_identifier;
use super::tree::SyntaxTree;
/// An expression.
#[derive(Clone, PartialEq)]
pub enum Expr {
/// An identifier: `ident`.
Ident(Ident),
/// A string: `"string"`.
Str(String),
/// A boolean: `true, false`.
Bool(bool),
/// A number: `1.2, 200%`.
Number(f64),
/// A length: `2cm, 5.2in`.
Length(Length),
/// A color value with alpha channel: `#f79143ff`.
Color(RgbaColor),
/// A syntax tree containing typesetting content.
Tree(SyntaxTree),
/// A table: `(false, 12cm, greeting="hi")`.
Table(TableExpr),
/// An operation that negates the contained expression.
Neg(Box<Spanned<Expr>>),
/// An operation that adds the contained expressions.
Add(Box<Spanned<Expr>>, Box<Spanned<Expr>>),
/// An operation that subtracts the contained expressions.
Sub(Box<Spanned<Expr>>, Box<Spanned<Expr>>),
/// An operation that multiplies the contained expressions.
Mul(Box<Spanned<Expr>>, Box<Spanned<Expr>>),
/// An operation that divides the contained expressions.
Div(Box<Spanned<Expr>>, Box<Spanned<Expr>>),
/// A function call: `cmyk(37.7, 0, 3.9, 1.1)`.
Call(FuncCall),
}
impl Expr {
/// A natural-language name of the type of this expression, e.g.
/// "identifier".
pub fn name(&self) -> &'static str {
use Expr::*;
match self {
Ident(_) => "identifier",
Str(_) => "string",
Bool(_) => "bool",
Number(_) => "number",
Length(_) => "length",
Color(_) => "color",
Tree(_) => "syntax tree",
Table(_) => "table",
Neg(_) => "negation",
Add(_, _) => "addition",
Sub(_, _) => "subtraction",
Mul(_, _) => "multiplication",
Div(_, _) => "division",
Call(_) => "function call",
}
}
}
impl Debug for Expr {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
use Expr::*;
match self {
Ident(i) => i.fmt(f),
Str(s) => s.fmt(f),
Bool(b) => b.fmt(f),
Number(n) => n.fmt(f),
Length(s) => s.fmt(f),
Color(c) => c.fmt(f),
Tree(t) => t.fmt(f),
Table(t) => t.fmt(f),
Neg(e) => write!(f, "-{:?}", e),
Add(a, b) => write!(f, "({:?} + {:?})", a, b),
Sub(a, b) => write!(f, "({:?} - {:?})", a, b),
Mul(a, b) => write!(f, "({:?} * {:?})", a, b),
Div(a, b) => write!(f, "({:?} / {:?})", a, b),
Call(c) => c.fmt(f),
}
}
}
/// An identifier as defined by unicode with a few extra permissible characters.
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Ident(pub String);
impl Ident {
/// Create a new identifier from a string checking that it is a valid.
pub fn new(ident: impl AsRef<str> + Into<String>) -> Option<Self> {
if is_identifier(ident.as_ref()) {
Some(Self(ident.into()))
} else {
None
}
}
/// Return a reference to the underlying string.
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl Debug for Ident {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "`{}`", self.0)
}
}
/// An 8-bit RGBA color.
///
/// # Example
/// ```typst
/// [page: background=#423abaff]
/// ^^^^^^^^
/// ```
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct RgbaColor {
/// Red channel.
pub r: u8,
/// Green channel.
pub g: u8,
/// Blue channel.
pub b: u8,
/// Alpha channel.
pub a: u8,
/// This is true if this value was provided as a fail-over by the parser
/// because the user-defined value was invalid. This color may be
/// overwritten if this property is true.
pub healed: bool,
}
impl RgbaColor {
/// Constructs a new color.
pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a, healed: false }
}
/// Constructs a new color with the healed property set to true.
pub fn new_healed(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a, healed: true }
}
}
impl FromStr for RgbaColor {
type Err = ParseColorError;
/// Constructs a new color from a hex string like `7a03c2`. Do not specify a
/// leading `#`.
fn from_str(hex_str: &str) -> Result<Self, Self::Err> {
if !hex_str.is_ascii() {
return Err(ParseColorError);
}
let len = hex_str.len();
let long = len == 6 || len == 8;
let short = len == 3 || len == 4;
let alpha = len == 4 || len == 8;
if !long && !short {
return Err(ParseColorError);
}
let mut values: [u8; 4] = [255; 4];
for elem in if alpha { 0..4 } else { 0..3 } {
let item_len = if long { 2 } else { 1 };
let pos = elem * item_len;
let item = &hex_str[pos..(pos+item_len)];
values[elem] = u8::from_str_radix(item, 16)
.map_err(|_| ParseColorError)?;
if short {
// Duplicate number for shorthand notation, i.e. `a` -> `aa`
values[elem] += values[elem] * 16;
}
}
Ok(Self::new(values[0], values[1], values[2], values[3]))
}
}
impl Debug for RgbaColor {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if f.alternate() {
write!(
f, "rgba({:02}, {:02}, {:02}, {:02})",
self.r, self.g, self.b, self.a,
)?;
} else {
write!(
f, "#{:02x}{:02x}{:02x}{:02x}",
self.r, self.g, self.b, self.a,
)?;
}
if self.healed {
f.write_str(" [healed]")?;
}
Ok(())
}
}
/// The error when parsing an `RgbaColor` fails.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct ParseColorError;
impl std::error::Error for ParseColorError {}
impl fmt::Display for ParseColorError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad("invalid color")
}
}
/// A table expression.
///
/// # Example
/// ```typst
/// (false, 12cm, greeting="hi")
/// ```
pub type TableExpr = Table<TableExprEntry>;
impl TableExpr {
/// Retrieve and remove the matching value with the lowest number key,
/// skipping and ignoring all non-matching entries with lower keys.
pub fn take<T: TryFromExpr>(&mut self) -> Option<T> {
for (&key, entry) in self.nums() {
let expr = entry.val.as_ref();
if let Some(val) = T::try_from_expr(expr, &mut Feedback::new()) {
self.remove(key);
return Some(val);
}
}
None
}
/// Retrieve and remove the matching value with the lowest number key,
/// removing and generating errors for all non-matching entries with lower
/// keys.
pub fn expect<T: TryFromExpr>(&mut self, f: &mut Feedback) -> Option<T> {
while let Some((num, _)) = self.first() {
let entry = self.remove(num).unwrap();
if let Some(val) = T::try_from_expr(entry.val.as_ref(), f) {
return Some(val);
}
}
None
}
/// Retrieve and remove a matching value associated with the given key if
/// there is any.
///
/// Generates an error if the key exists but the value does not match.
pub fn take_with_key<'a, K, T>(&mut self, key: K, f: &mut Feedback) -> Option<T>
where
K: Into<BorrowedKey<'a>>,
T: TryFromExpr,
{
self.remove(key).and_then(|entry| {
let expr = entry.val.as_ref();
T::try_from_expr(expr, f)
})
}
/// Retrieve and remove all matching pairs with number keys, skipping and
/// ignoring non-matching entries.
///
/// The pairs are returned in order of increasing keys.
pub fn take_all_num<'a, T>(&'a mut self) -> impl Iterator<Item = (u64, T)> + 'a
where
T: TryFromExpr,
{
let mut skip = 0;
std::iter::from_fn(move || {
for (&key, entry) in self.nums().skip(skip) {
let expr = entry.val.as_ref();
if let Some(val) = T::try_from_expr(expr, &mut Feedback::new()) {
self.remove(key);
return Some((key, val));
}
skip += 1;
}
None
})
}
/// Retrieve and remove all matching values with number keys, skipping and
/// ignoring non-matching entries.
///
/// The values are returned in order of increasing keys.
pub fn take_all_num_vals<'a, T: 'a>(&'a mut self) -> impl Iterator<Item = T> + 'a
where
T: TryFromExpr,
{
self.take_all_num::<T>().map(|(_, v)| v)
}
/// Retrieve and remove all matching pairs with string keys, skipping and
/// ignoring non-matching entries.
///
/// The pairs are returned in order of increasing keys.
pub fn take_all_str<'a, T>(&'a mut self) -> impl Iterator<Item = (String, T)> + 'a
where
T: TryFromExpr,
{
let mut skip = 0;
std::iter::from_fn(move || {
for (key, entry) in self.strs().skip(skip) {
let expr = entry.val.as_ref();
if let Some(val) = T::try_from_expr(expr, &mut Feedback::new()) {
let key = key.clone();
self.remove(&key);
return Some((key, val));
}
skip += 1;
}
None
})
}
/// Generated `"unexpected argument"` errors for all remaining entries.
pub fn unexpected(&self, f: &mut Feedback) {
for entry in self.values() {
let span = Span::merge(entry.key, entry.val.span);
error!(@f, span, "unexpected argument");
}
}
}
/// An entry in a table expression.
///
/// Contains the key's span and the value.
#[derive(Clone, PartialEq)]
pub struct TableExprEntry {
pub key: Span,
pub val: Spanned<Expr>,
}
impl TableExprEntry {
/// Create a new entry.
pub fn new(key: Span, val: Spanned<Expr>) -> Self {
Self { key, val }
}
/// Create an entry for a positional argument with the same span for key and
/// value.
pub fn val(val: Spanned<Expr>) -> Self {
Self { key: Span::ZERO, val }
}
}
impl Debug for TableExprEntry {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if f.alternate() {
f.write_str("key")?;
self.key.fmt(f)?;
f.write_str(" ")?;
}
self.val.fmt(f)
}
}
/// A trait for converting expressions into specific types.
pub trait TryFromExpr: Sized {
// This trait takes references because we don't want to move the expression
// out of its origin in case this returns `None`. This solution is not
// perfect because we need to do some cloning in the impls for this trait,
// but I haven't got a better solution, for now.
/// Try to convert an expression into this type.
///
/// Returns `None` and generates an appropriate error if the expression is
/// not valid for this type.
fn try_from_expr(expr: Spanned<&Expr>, f: &mut Feedback) -> Option<Self>;
}
macro_rules! impl_match {
($type:ty, $name:expr, $($p:pat => $r:expr),* $(,)?) => {
impl TryFromExpr for $type {
fn try_from_expr(expr: Spanned<&Expr>, f: &mut Feedback) -> Option<Self> {
#[allow(unreachable_patterns)]
match expr.v {
$($p => Some($r)),*,
other => {
error!(
@f, expr.span,
"expected {}, found {}", $name, other.name()
);
None
}
}
}
}
};
}
macro_rules! impl_ident {
($type:ty, $name:expr, $parse:expr) => {
impl TryFromExpr for $type {
fn try_from_expr(expr: Spanned<&Expr>, f: &mut Feedback) -> Option<Self> {
if let Expr::Ident(ident) = expr.v {
let val = $parse(ident.as_str());
if val.is_none() {
error!(@f, expr.span, "invalid {}", $name);
}
val
} else {
error!(
@f, expr.span,
"expected {}, found {}", $name, expr.v.name()
);
None
}
}
}
};
}
impl<T: TryFromExpr> TryFromExpr for Spanned<T> {
fn try_from_expr(expr: Spanned<&Expr>, f: &mut Feedback) -> Option<Self> {
let span = expr.span;
T::try_from_expr(expr, f).map(|v| Spanned { v, span })
}
}
impl_match!(Expr, "expression", e => e.clone());
impl_match!(Ident, "identifier", Expr::Ident(i) => i.clone());
impl_match!(String, "string", Expr::Str(s) => s.clone());
impl_match!(bool, "bool", Expr::Bool(b) => b.clone());
impl_match!(f64, "number", Expr::Number(n) => n.clone());
impl_match!(Length, "length", Expr::Length(l) => l.clone());
impl_match!(SyntaxTree, "tree", Expr::Tree(t) => t.clone());
impl_match!(TableExpr, "table", Expr::Table(t) => t.clone());
impl_match!(ScaleLength, "number or length",
&Expr::Length(length) => ScaleLength::Absolute(length),
&Expr::Number(scale) => ScaleLength::Scaled(scale),
);
/// A value type that matches identifiers and strings and implements
/// `Into<String>`.
pub struct StringLike(pub String);
impl From<StringLike> for String {
fn from(like: StringLike) -> String {
like.0
}
}
impl_match!(StringLike, "identifier or string",
Expr::Ident(Ident(s)) => StringLike(s.clone()),
Expr::Str(s) => StringLike(s.clone()),
);
impl_ident!(Dir, "direction", |s| match s {
"ltr" => Some(Self::LTR),
"rtl" => Some(Self::RTL),
"ttb" => Some(Self::TTB),
"btt" => Some(Self::BTT),
_ => None,
});
impl_ident!(SpecAlign, "alignment", |s| match s {
"left" => Some(Self::Left),
"right" => Some(Self::Right),
"top" => Some(Self::Top),
"bottom" => Some(Self::Bottom),
"center" => Some(Self::Center),
_ => None,
});
impl_ident!(FontStyle, "font style", FontStyle::from_name);
impl_ident!(Paper, "paper", Paper::from_name);
impl TryFromExpr for FontWeight {
fn try_from_expr(expr: Spanned<&Expr>, f: &mut Feedback) -> Option<Self> {
match expr.v {
&Expr::Number(weight) => {
const MIN: u16 = 100;
const MAX: u16 = 900;
Some(Self(if weight < MIN as f64 {
error!(@f, expr.span, "the minimum font weight is {}", MIN);
MIN
} else if weight > MAX as f64 {
error!(@f, expr.span, "the maximum font weight is {}", MAX);
MAX
} else {
weight.round() as u16
}))
}
Expr::Ident(ident) => {
let weight = Self::from_name(ident.as_str());
if weight.is_none() {
error!(@f, expr.span, "invalid font weight");
}
weight
}
other => {
error!(
@f, expr.span,
"expected font weight (name or number), found {}",
other.name(),
);
None
}
}
}
}
impl TryFromExpr for FontWidth {
fn try_from_expr(expr: Spanned<&Expr>, f: &mut Feedback) -> Option<Self> {
match expr.v {
&Expr::Number(width) => {
const MIN: u16 = 1;
const MAX: u16 = 9;
Self::new(if width < MIN as f64 {
error!(@f, expr.span, "the minimum font width is {}", MIN);
MIN
} else if width > MAX as f64 {
error!(@f, expr.span, "the maximum font width is {}", MAX);
MAX
} else {
width.round() as u16
})
}
Expr::Ident(ident) => {
let width = Self::from_name(ident.as_str());
if width.is_none() {
error!(@f, expr.span, "invalid font width");
}
width
}
other => {
error!(
@f, expr.span,
"expected font width (name or number), found {}",
other.name(),
);
None
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn 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)),
);
}
test("f61243ff", 0xf6, 0x12, 0x43, 0xff);
test("b3d8b3", 0xb3, 0xd8, 0xb3, 0xff);
test("fCd2a9AD", 0xfc, 0xd2, 0xa9, 0xad);
test("233", 0x22, 0x33, 0x33, 0xff);
test("111b", 0x11, 0x11, 0x11, 0xbb);
}
fn entry(expr: Expr) -> TableExprEntry {
TableExprEntry {
key: Span::ZERO,
val: Spanned::zero(expr),
}
}
#[test]
fn test_table_take_removes_correct_entry() {
let mut table = TableExpr::new();
table.insert(1, entry(Expr::Bool(false)));
table.insert(2, entry(Expr::Str("hi".to_string())));
assert_eq!(table.take::<String>(), Some("hi".to_string()));
assert_eq!(table.len(), 1);
assert_eq!(table.take::<bool>(), Some(false));
assert!(table.is_empty());
}
#[test]
fn test_table_expect_errors_about_previous_entries() {
let mut f = Feedback::new();
let mut table = TableExpr::new();
table.insert(1, entry(Expr::Bool(false)));
table.insert(3, entry(Expr::Str("hi".to_string())));
table.insert(5, entry(Expr::Bool(true)));
assert_eq!(table.expect::<String>(&mut f), Some("hi".to_string()));
assert_eq!(f.diagnostics, [error!(Span::ZERO, "expected string, found bool")]);
assert_eq!(table.len(), 1);
}
#[test]
fn test_table_take_with_key_removes_the_entry() {
let mut f = Feedback::new();
let mut table = TableExpr::new();
table.insert(1, entry(Expr::Bool(false)));
table.insert("hi", entry(Expr::Bool(true)));
assert_eq!(table.take_with_key::<_, bool>(1, &mut f), Some(false));
assert_eq!(table.take_with_key::<_, f64>("hi", &mut f), None);
assert_eq!(f.diagnostics, [error!(Span::ZERO, "expected number, found bool")]);
assert!(table.is_empty());
}
#[test]
fn test_table_take_all_removes_the_correct_entries() {
let mut table = TableExpr::new();
table.insert(1, entry(Expr::Bool(false)));
table.insert(3, entry(Expr::Number(0.0)));
table.insert(7, entry(Expr::Bool(true)));
assert_eq!(
table.take_all_num::<bool>().collect::<Vec<_>>(),
[(1, false), (7, true)],
);
assert_eq!(table.len(), 1);
assert_eq!(table[3].val.v, Expr::Number(0.0));
}
}

View File

@ -1,17 +1,44 @@
//! Syntax trees, parsing and tokenization.
pub mod decoration;
pub mod expr;
pub mod parsing;
pub mod scope;
pub mod span;
pub mod tokens;
pub mod tree;
use std::fmt::{self, Debug, Formatter};
use tokens::is_identifier;
/// An identifier as defined by unicode with a few extra permissible characters.
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Ident(pub String);
impl Ident {
/// Create a new identifier from a string checking that it is a valid.
pub fn new(ident: impl AsRef<str> + Into<String>) -> Option<Self> {
if is_identifier(ident.as_ref()) {
Some(Self(ident.into()))
} else {
None
}
}
/// Return a reference to the underlying string.
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl Debug for Ident {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "`{}`", self.0)
}
}
#[cfg(test)]
mod tests {
use std::fmt::Debug;
use crate::func::prelude::*;
use crate::prelude::*;
use super::span;
/// Assert that expected and found are equal, printing both and panicking
@ -49,18 +76,4 @@ mod tests {
Spanned::zero(t)
}
}
pub fn debug_func(call: FuncCall, _: &ParseState) -> Pass<SyntaxNode> {
Pass::node(DebugNode(call), Feedback::new())
}
#[derive(Debug, Clone, PartialEq)]
pub struct DebugNode(pub FuncCall);
#[async_trait(?Send)]
impl Layout for DebugNode {
async fn layout<'a>(&'a self, _: LayoutContext<'_>) -> Pass<Commands<'a>> {
unimplemented!()
}
}
}

View File

@ -3,31 +3,13 @@
use std::str::FromStr;
use crate::{Feedback, Pass};
use crate::color::RgbaColor;
use crate::compute::table::SpannedEntry;
use super::decoration::Decoration;
use super::expr::*;
use super::scope::Scope;
use super::span::{Pos, Span, Spanned};
use super::tokens::{is_newline_char, Token, TokenMode, Tokens};
use super::tree::{SyntaxNode, SyntaxTree};
/// A function which parses a function call into a dynamic node.
pub type CallParser = dyn Fn(FuncCall, &ParseState) -> Pass<SyntaxNode>;
/// An invocation of a function.
#[derive(Debug, Clone, PartialEq)]
pub struct FuncCall {
pub name: Spanned<Ident>,
pub args: TableExpr,
}
/// The state which can influence how a string of source code is parsed.
///
/// Parsing is pure - when passed in the same state and source code, the output
/// must be the same.
pub struct ParseState {
/// The scope containing all function definitions.
pub scope: Scope,
}
use super::tree::{CallExpr, Expr, SyntaxNode, SyntaxTree, TableExpr};
use super::Ident;
/// Parse a string of source code.
///
@ -35,7 +17,7 @@ pub struct ParseState {
/// `offset` position. This is used to make spans of a function body relative to
/// the start of the function as a whole as opposed to the start of the
/// function's body.
pub fn parse(src: &str, offset: Pos, state: &ParseState) -> Pass<SyntaxTree> {
pub fn parse(src: &str, offset: Pos) -> Pass<SyntaxTree> {
let mut tree = SyntaxTree::new();
let mut par = SyntaxTree::new();
let mut feedback = Feedback::new();
@ -58,12 +40,12 @@ pub fn parse(src: &str, offset: Pos, state: &ParseState) -> Pass<SyntaxTree> {
}
Token::Function { header, body, terminated } => {
let parsed = FuncParser::new(header, body, state).parse();
let parsed = FuncParser::new(header, body).parse();
feedback.extend_offset(parsed.feedback, span.start);
if !terminated {
error!(@feedback, Span::at(span.end), "expected closing bracket");
}
parsed.output
SyntaxNode::Call(parsed.output)
}
Token::Star => SyntaxNode::ToggleBolder,
@ -97,8 +79,6 @@ pub fn parse(src: &str, offset: Pos, state: &ParseState) -> Pass<SyntaxTree> {
}
struct FuncParser<'s> {
state: &'s ParseState,
/// The tokens inside the header.
tokens: Tokens<'s>,
peeked: Option<Option<Spanned<Token<'s>>>>,
body: Option<Spanned<&'s str>>,
@ -106,13 +86,8 @@ struct FuncParser<'s> {
}
impl<'s> FuncParser<'s> {
fn new(
header: &'s str,
body: Option<Spanned<&'s str>>,
state: &'s ParseState,
) -> Self {
fn new(header: &'s str, body: Option<Spanned<&'s str>>) -> Self {
Self {
state,
// Start at column 1 because the opening bracket is also part of
// the function, but not part of the `header` string.
tokens: Tokens::new(header, Pos::new(0, 1), TokenMode::Header),
@ -122,65 +97,17 @@ impl<'s> FuncParser<'s> {
}
}
fn parse(mut self) -> Pass<SyntaxNode> {
let (parser, mut call) = if let Some(call) = self.parse_func_header() {
let name = call.name.v.as_str();
let (parser, deco) = match self.state.scope.get_parser(name) {
// The function exists in the scope.
Some(parser) => (parser, Decoration::ResolvedFunc),
// The function does not exist in the scope. The parser that is
// returned here is a fallback parser which exists to make sure
// the content of the function is not totally dropped (on a best
// effort basis).
None => {
error!(@self.feedback, call.name.span, "unknown function");
let parser = self.state.scope.get_fallback_parser();
(parser, Decoration::UnresolvedFunc)
}
};
self.feedback.decorations.push(Spanned::new(deco, call.name.span));
(parser, call)
} else {
// Parse the call with the fallback parser even when the header is
// completely unparsable.
let parser = self.state.scope.get_fallback_parser();
let call = FuncCall {
name: Spanned::new(Ident(String::new()), Span::ZERO),
args: TableExpr::new(),
};
(parser, call)
};
if let Some(body) = self.body {
call.args.push(TableExprEntry {
key: Span::ZERO,
val: body.map(|src| {
let parsed = parse(src, body.span.start, &self.state);
self.feedback.extend(parsed.feedback);
Expr::Tree(parsed.output)
}),
});
}
let parsed = parser(call, self.state);
self.feedback.extend(parsed.feedback);
Pass::new(parsed.output, self.feedback)
}
fn parse_func_header(&mut self) -> Option<FuncCall> {
fn parse(mut self) -> Pass<CallExpr> {
let after_bracket = self.pos();
self.skip_white();
let name = try_opt_or!(self.parse_ident(), {
let name = self.parse_ident().unwrap_or_else(|| {
self.expected_found_or_at("function name", after_bracket);
return None;
Spanned::zero(Ident(String::new()))
});
self.skip_white();
let args = match self.eat().map(Spanned::value) {
let mut args = match self.eat().map(Spanned::value) {
Some(Token::Colon) => self.parse_table(false).0.v,
Some(_) => {
self.expected_at("colon", name.span.end);
@ -189,7 +116,15 @@ impl<'s> FuncParser<'s> {
None => TableExpr::new(),
};
Some(FuncCall { name, args })
if let Some(body) = self.body {
args.push(SpannedEntry::val(body.map(|src| {
let parsed = parse(src, body.span.start);
self.feedback.extend(parsed.feedback);
Expr::Tree(parsed.output)
})));
}
Pass::new(CallExpr { name, args }, self.feedback)
}
}
@ -325,14 +260,20 @@ impl FuncParser<'_> {
}
}
fn parse_func_call(&mut self, name: Spanned<Ident>) -> Spanned<FuncCall> {
fn parse_func_call(&mut self, name: Spanned<Ident>) -> Spanned<CallExpr> {
let args = self.parse_table(true).0;
let span = Span::merge(name.span, args.span);
Spanned::new(FuncCall { name, args: args.v }, span)
Spanned::new(CallExpr { name, args: args.v }, span)
}
/// The boolean tells you whether the table can be coerced into an expression
/// (this is the case when it's length 1 and has no trailing comma).
/// Set `parens` to true, when this should expect an opening paren and stop
/// at the balanced closing paren (this is the case for normal tables and
/// round-paren function calls). Set it to false, when this is used to parse
/// the top-level function arguments.
///
/// The returned boolean tells you whether the table can be coerced into an
/// expression (this is the case when it's length 1 and has no trailing
/// comma).
fn parse_table(&mut self, parens: bool) -> (Spanned<TableExpr>, bool) {
let start = self.pos();
if parens {
@ -369,19 +310,19 @@ impl FuncParser<'_> {
coercable = false;
behind_arg = val.span.end;
table.insert(key.v.0, TableExprEntry::new(key.span, val));
table.insert(key.v.0, SpannedEntry::new(key.span, val));
} else if self.check(Token::LeftParen) {
let call = self.parse_func_call(ident);
let expr = call.map(|call| Expr::Call(call));
behind_arg = expr.span.end;
table.push(TableExprEntry::val(expr));
table.push(SpannedEntry::val(expr));
} else {
let expr = ident.map(|id| Expr::Ident(id));
behind_arg = expr.span.end;
table.push(TableExprEntry::val(expr));
table.push(SpannedEntry::val(expr));
}
} else {
// It's a positional argument.
@ -390,7 +331,7 @@ impl FuncParser<'_> {
continue;
});
behind_arg = expr.span.end;
table.push(TableExprEntry::val(expr));
table.push(SpannedEntry::val(expr));
}
self.skip_white();
@ -593,7 +534,7 @@ mod tests {
}
macro_rules! F {
($($tts:tt)*) => { SyntaxNode::boxed(DebugNode(Call!(@$($tts)*))) }
($($tts:tt)*) => { SyntaxNode::Call(Call!(@$($tts)*)) }
}
// ------------------------ Construct Expressions ----------------------- //
@ -603,24 +544,17 @@ mod tests {
fn Id(ident: &str) -> Expr { Expr::Ident(Ident(ident.to_string())) }
fn Str(string: &str) -> Expr { Expr::Str(string.to_string()) }
macro_rules! Tree {
(@$($node:expr),* $(,)?) => {
vec![$(Into::<Spanned<SyntaxNode>>::into($node)),*]
};
($($tts:tt)*) => { Expr::Tree(Tree![@$($tts)*]) };
}
macro_rules! Table {
(@table=$table:expr,) => {};
(@table=$table:expr, $key:expr => $value:expr $(, $($tts:tt)*)?) => {{
let key = Into::<Spanned<&str>>::into($key);
let val = Into::<Spanned<Expr>>::into($value);
$table.insert(key.v, TableExprEntry::new(key.span, val));
$table.insert(key.v, SpannedEntry::new(key.span, val));
Table![@table=$table, $($($tts)*)?];
}};
(@table=$table:expr, $value:expr $(, $($tts:tt)*)?) => {
let val = Into::<Spanned<Expr>>::into($value);
$table.push(TableExprEntry::val(val));
$table.push(SpannedEntry::val(val));
Table![@table=$table, $($($tts)*)?];
};
(@$($tts:tt)*) => {{
@ -632,6 +566,24 @@ mod tests {
($($tts:tt)*) => { Expr::Table(Table![@$($tts)*]) };
}
macro_rules! Tree {
(@$($node:expr),* $(,)?) => {
vec![$(Into::<Spanned<SyntaxNode>>::into($node)),*]
};
($($tts:tt)*) => { Expr::Tree(Tree![@$($tts)*]) };
}
macro_rules! Call {
(@$name:expr $(; $($tts:tt)*)?) => {{
let name = Into::<Spanned<&str>>::into($name);
CallExpr {
name: name.map(|n| Ident(n.to_string())),
args: Table![@$($($tts)*)?],
}
}};
($($tts:tt)*) => { Expr::Call(Call![@$($tts)*]) };
}
fn Neg<T: Into<Spanned<Expr>>>(e1: T) -> Expr {
Expr::Neg(Box::new(e1.into()))
}
@ -648,17 +600,6 @@ mod tests {
Expr::Div(Box::new(e1.into()), Box::new(e2.into()))
}
macro_rules! Call {
(@$name:expr $(; $($tts:tt)*)?) => {{
let name = Into::<Spanned<&str>>::into($name);
FuncCall {
name: name.map(|n| Ident(n.to_string())),
args: Table![@$($($tts)*)?],
}
}};
($($tts:tt)*) => { Expr::Call(Call![@$($tts)*]) };
}
// ----------------------------- Test Macros ---------------------------- //
// Test syntax trees with or without spans.
@ -667,7 +608,7 @@ mod tests {
macro_rules! test {
(@spans=$spans:expr, $src:expr => $($tts:tt)*) => {
let exp = Tree![@$($tts)*];
let pass = parse_default($src);
let pass = parse($src, Pos::ZERO);
check($src, exp, pass.output, $spans);
};
}
@ -683,7 +624,7 @@ mod tests {
macro_rules! e {
($src:expr => $($tts:tt)*) => {
let exp = vec![$($tts)*];
let pass = parse_default($src);
let pass = parse($src, Pos::ZERO);
let found = pass.feedback.diagnostics.iter()
.map(|s| s.as_ref().map(|e| e.message.as_str()))
.collect::<Vec<_>>();
@ -695,20 +636,11 @@ mod tests {
macro_rules! d {
($src:expr => $($tts:tt)*) => {
let exp = vec![$($tts)*];
let pass = parse_default($src);
let pass = parse($src, Pos::ZERO);
check($src, exp, pass.feedback.decorations, true);
};
}
fn parse_default(src: &str) -> Pass<SyntaxTree> {
let mut scope = Scope::new(Box::new(debug_func));
scope.insert("box", Box::new(debug_func));
scope.insert("val", Box::new(debug_func));
scope.insert("f", Box::new(debug_func));
let state = ParseState { scope };
parse(src, Pos::ZERO, &state)
}
// -------------------------------- Tests ------------------------------- //
#[test]
@ -797,15 +729,9 @@ mod tests {
e!("[\"]" => s(0,1, 0,3, "expected function name, found string"),
s(0,3, 0,3, "expected closing bracket"));
// An unknown name.
t!("[hi]" => P![F!("hi")]);
e!("[hi]" => s(0,1, 0,3, "unknown function"));
d!("[hi]" => s(0,1, 0,3, UnresolvedFunc));
// A valid name.
t!("[f]" => P![F!("f")]);
t!("[hi]" => P![F!("hi")]);
t!("[ f]" => P![F!("f")]);
d!("[ f]" => s(0,3, 0,4, ResolvedFunc));
// An invalid name.
e!("[12]" => s(0,1, 0,3, "expected function name, found number"));
@ -821,7 +747,6 @@ mod tests {
t!("[val=]" => P![F!("val")]);
e!("[val=]" => s(0,4, 0,4, "expected colon"));
e!("[val/🌎:$]" => s(0,4, 0,4, "expected colon"));
d!("[val=]" => s(0,1, 0,4, ResolvedFunc));
// String in invalid header without colon still parsed as string
// Note: No "expected quote" error because not even the string was
@ -939,9 +864,9 @@ mod tests {
v!("(1, key=\"value\")" => Table![Num(1.0), "key" => Str("value")]);
// Decorations.
d!("[val: key=hi]" => s(0,6, 0,9, TableKey), s(0,1, 0,4, ResolvedFunc));
d!("[val: (key=hi)]" => s(0,7, 0,10, TableKey), s(0,1, 0,4, ResolvedFunc));
d!("[val: f(key=hi)]" => s(0,8, 0,11, TableKey), s(0,1, 0,4, ResolvedFunc));
d!("[val: key=hi]" => s(0,6, 0,9, TableKey));
d!("[val: (key=hi)]" => s(0,7, 0,10, TableKey));
d!("[val: f(key=hi)]" => s(0,8, 0,11, TableKey));
// Spanned with spacing around keyword arguments.
ts!("[val: \n hi \n = /* //\n */ \"s\n\"]" => s(0,0, 4,2, P![

View File

@ -1,44 +0,0 @@
//! Mapping of function names to function parsers.
use std::collections::HashMap;
use std::fmt::{self, Debug, Formatter};
use super::parsing::CallParser;
/// A map from identifiers to function parsers.
pub struct Scope {
parsers: HashMap<String, Box<CallParser>>,
fallback: Box<CallParser>,
}
impl Scope {
/// Create a new empty scope with a fallback parser that is invoked when no
/// match is found.
pub fn new(fallback: Box<CallParser>) -> Self {
Self {
parsers: HashMap::new(),
fallback,
}
}
/// Associate the given function name with a dynamic node type.
pub fn insert(&mut self, name: impl Into<String>, parser: Box<CallParser>) {
self.parsers.insert(name.into(), parser);
}
/// Return the parser with the given name if there is one.
pub fn get_parser(&self, name: &str) -> Option<&CallParser> {
self.parsers.get(name).map(AsRef::as_ref)
}
/// Return the fallback parser.
pub fn get_fallback_parser(&self) -> &CallParser {
&*self.fallback
}
}
impl Debug for Scope {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_set().entries(self.parsers.keys()).finish()
}
}

View File

@ -39,13 +39,11 @@ impl<T> Offset for SpanVec<T> {
}
/// A value with the span it corresponds to in the source code.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[cfg_attr(feature = "serialize", derive(Serialize))]
pub struct Spanned<T> {
/// The value.
pub v: T,
/// The corresponding span.
pub span: Span,
pub v: T,
}
impl<T> Spanned<T> {
@ -99,7 +97,7 @@ impl<T: Debug> Debug for Spanned<T> {
}
/// Locates a slice of source code.
#[derive(Copy, Clone, Hash)]
#[derive(Copy, Clone, Ord, PartialOrd, Hash)]
#[cfg_attr(feature = "serialize", derive(Serialize))]
pub struct Span {
/// The inclusive start position.

View File

@ -1,17 +1,20 @@
//! The syntax tree.
use std::any::Any;
use std::fmt::Debug;
use std::fmt::{self, Debug, Formatter};
use crate::layout::Layout;
use super::span::SpanVec;
use crate::color::RgbaColor;
use crate::compute::table::{SpannedEntry, Table};
use crate::compute::value::{TableValue, Value};
use crate::length::Length;
use super::span::{Spanned, SpanVec};
use super::Ident;
/// A collection of nodes which form a tree together with the nodes' children.
pub type SyntaxTree = SpanVec<SyntaxNode>;
/// A syntax node, which encompasses a single logical entity of parsed source
/// code.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub enum SyntaxNode {
/// Whitespace containing less than two newlines.
Spacing,
@ -27,84 +30,138 @@ pub enum SyntaxNode {
Raw(Vec<String>),
/// A paragraph of child nodes.
Par(SyntaxTree),
/// A dynamic node, created through function invocations in source code.
Dyn(Box<dyn DynamicNode>),
/// A function call.
Call(CallExpr),
}
impl SyntaxNode {
/// Create a `Dyn` variant from an unboxed dynamic node.
pub fn boxed<T: DynamicNode + 'static>(node: T) -> SyntaxNode {
SyntaxNode::Dyn(Box::new(node))
/// An expression.
#[derive(Clone, PartialEq)]
pub enum Expr {
/// An identifier: `ident`.
Ident(Ident),
/// A string: `"string"`.
Str(String),
/// A boolean: `true, false`.
Bool(bool),
/// A number: `1.2, 200%`.
Number(f64),
/// A length: `2cm, 5.2in`.
Length(Length),
/// A color value with alpha channel: `#f79143ff`.
Color(RgbaColor),
/// A table expression: `(false, 12cm, greeting="hi")`.
Table(TableExpr),
/// A syntax tree containing typesetting content.
Tree(SyntaxTree),
/// A function call expression: `cmyk(37.7, 0, 3.9, 1.1)`.
Call(CallExpr),
/// An operation that negates the contained expression.
Neg(Box<Spanned<Expr>>),
/// An operation that adds the contained expressions.
Add(Box<Spanned<Expr>>, Box<Spanned<Expr>>),
/// An operation that subtracts the contained expressions.
Sub(Box<Spanned<Expr>>, Box<Spanned<Expr>>),
/// An operation that multiplies the contained expressions.
Mul(Box<Spanned<Expr>>, Box<Spanned<Expr>>),
/// An operation that divides the contained expressions.
Div(Box<Spanned<Expr>>, Box<Spanned<Expr>>),
}
impl Expr {
/// A natural-language name of the type of this expression, e.g.
/// "identifier".
pub fn name(&self) -> &'static str {
use Expr::*;
match self {
Ident(_) => "identifier",
Str(_) => "string",
Bool(_) => "bool",
Number(_) => "number",
Length(_) => "length",
Color(_) => "color",
Table(_) => "table",
Tree(_) => "syntax tree",
Call(_) => "function call",
Neg(_) => "negation",
Add(_, _) => "addition",
Sub(_, _) => "subtraction",
Mul(_, _) => "multiplication",
Div(_, _) => "division",
}
}
}
impl PartialEq for SyntaxNode {
fn eq(&self, other: &SyntaxNode) -> bool {
use SyntaxNode::*;
match (self, other) {
(Spacing, Spacing) => true,
(Linebreak, Linebreak) => true,
(ToggleItalic, ToggleItalic) => true,
(ToggleBolder, ToggleBolder) => true,
(Text(a), Text(b)) => a == b,
(Raw(a), Raw(b)) => a == b,
(Par(a), Par(b)) => a == b,
(Dyn(a), Dyn(b)) => a == b,
_ => false,
/// Evaluate the expression to a value.
pub fn eval(&self) -> Value {
use Expr::*;
match self {
Ident(i) => Value::Ident(i.clone()),
Str(s) => Value::Str(s.clone()),
&Bool(b) => Value::Bool(b),
&Number(n) => Value::Number(n),
&Length(s) => Value::Length(s),
&Color(c) => Value::Color(c),
Table(t) => Value::Table(t.eval()),
Tree(t) => Value::Tree(t.clone()),
Call(_) => todo!("eval call"),
Neg(_) => todo!("eval neg"),
Add(_, _) => todo!("eval add"),
Sub(_, _) => todo!("eval sub"),
Mul(_, _) => todo!("eval mul"),
Div(_, _) => todo!("eval div"),
}
}
}
/// Dynamic syntax nodes.
impl Debug for Expr {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
use Expr::*;
match self {
Ident(i) => i.fmt(f),
Str(s) => s.fmt(f),
Bool(b) => b.fmt(f),
Number(n) => n.fmt(f),
Length(s) => s.fmt(f),
Color(c) => c.fmt(f),
Table(t) => t.fmt(f),
Tree(t) => t.fmt(f),
Call(c) => c.fmt(f),
Neg(e) => write!(f, "-{:?}", e),
Add(a, b) => write!(f, "({:?} + {:?})", a, b),
Sub(a, b) => write!(f, "({:?} - {:?})", a, b),
Mul(a, b) => write!(f, "({:?} * {:?})", a, b),
Div(a, b) => write!(f, "({:?} / {:?})", a, b),
}
}
}
/// A table of expressions.
///
/// *Note*: This is automatically implemented for all types which are
/// `Debug + Clone + PartialEq`, `Layout` and `'static`.
pub trait DynamicNode: Debug + Layout {
/// Convert into a `dyn Any`.
fn as_any(&self) -> &dyn Any;
/// # Example
/// ```typst
/// (false, 12cm, greeting="hi")
/// ```
pub type TableExpr = Table<SpannedEntry<Expr>>;
/// Check for equality with another dynamic node.
fn dyn_eq(&self, other: &dyn DynamicNode) -> bool;
impl TableExpr {
/// Evaluate the table expression to a table value.
pub fn eval(&self) -> TableValue {
let mut table = TableValue::new();
/// Clone into a boxed node trait object.
fn box_clone(&self) -> Box<dyn DynamicNode>;
}
impl dyn DynamicNode {
/// Downcast this dynamic node to a concrete node.
pub fn downcast<T: DynamicNode + 'static>(&self) -> Option<&T> {
self.as_any().downcast_ref::<T>()
}
}
impl PartialEq for dyn DynamicNode {
fn eq(&self, other: &Self) -> bool {
self.dyn_eq(other)
}
}
impl Clone for Box<dyn DynamicNode> {
fn clone(&self) -> Self {
self.box_clone()
}
}
impl<T> DynamicNode for T
where
T: Debug + PartialEq + Clone + Layout + 'static,
{
fn as_any(&self) -> &dyn Any {
self
}
fn dyn_eq(&self, other: &dyn DynamicNode) -> bool {
match other.as_any().downcast_ref::<Self>() {
Some(other) => self == other,
None => false,
for (&key, entry) in self.nums() {
table.insert(key, entry.as_ref().map(|val| val.eval()));
}
}
fn box_clone(&self) -> Box<dyn DynamicNode> {
Box::new(self.clone())
for (key, entry) in self.strs() {
table.insert(key.clone(), entry.as_ref().map(|val| val.eval()));
}
table
}
}
/// An invocation of a function.
#[derive(Debug, Clone, PartialEq)]
pub struct CallExpr {
pub name: Spanned<Ident>,
pub args: TableExpr,
}

View File

@ -87,7 +87,10 @@ fn test(
let typeset = block_on(typesetter.typeset(src));
let layouts = typeset.output;
for diagnostic in typeset.feedback.diagnostics {
let mut feedback = typeset.feedback;
feedback.diagnostics.sort();
for diagnostic in feedback.diagnostics {
println!(
" {:?} {:?}: {}",
diagnostic.v.level, diagnostic.span, diagnostic.v.message,