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:
parent
9f6137d8a8
commit
30f16bbf64
@ -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"
|
||||
|
@ -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
131
src/color.rs
Normal 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
5
src/compute/mod.rs
Normal 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
50
src/compute/scope.rs
Normal 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()
|
||||
}
|
||||
}
|
@ -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
474
src/compute/value.rs
Normal 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));
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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),
|
||||
|
@ -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),
|
||||
|
31
src/lib.rs
31
src/lib.rs
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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![],
|
||||
})
|
||||
}
|
||||
}
|
@ -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 {
|
@ -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,
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
@ -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!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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![
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user