Skip to content

Commit 8cddf11

Browse files
committed
feat: evaluate simple constant expressions for columns
1 parent 3eb0f05 commit 8cddf11

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+556
-194
lines changed

crates/typstyle-core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod attr;
22
pub mod ext;
3+
pub mod liteval;
34
pub mod partial;
45
pub mod pretty;
56

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
//! Evaluate simple constant Typst expressions in code mode without scopes or VMs.
2+
//!
3+
//! Currently, this is only used for determine table columns.
4+
5+
use typst_syntax::ast::*;
6+
7+
#[derive(Debug, Clone, PartialEq, Eq)]
8+
pub enum Value {
9+
None,
10+
Auto,
11+
Int(i64),
12+
/// Only represented by length.
13+
Array(usize),
14+
}
15+
16+
#[derive(Debug, Clone, PartialEq, Eq)]
17+
pub enum EvalError {
18+
NotSupported,
19+
InvalidOperation,
20+
}
21+
22+
pub type EvalResult = Result<Value, EvalError>;
23+
24+
pub trait Liteval {
25+
fn liteval(&self) -> EvalResult;
26+
}
27+
28+
impl Liteval for Expr<'_> {
29+
fn liteval(&self) -> EvalResult {
30+
match self {
31+
Expr::None(v) => v.liteval(),
32+
Expr::Auto(v) => v.liteval(),
33+
Expr::Int(v) => v.liteval(),
34+
Expr::Parenthesized(v) => v.liteval(),
35+
Expr::Array(v) => v.liteval(),
36+
Expr::Unary(v) => v.liteval(),
37+
Expr::Binary(v) => v.liteval(),
38+
_ => Err(EvalError::NotSupported),
39+
}
40+
}
41+
}
42+
43+
impl Liteval for None<'_> {
44+
fn liteval(&self) -> EvalResult {
45+
Ok(Value::None)
46+
}
47+
}
48+
49+
impl Liteval for Auto<'_> {
50+
fn liteval(&self) -> EvalResult {
51+
Ok(Value::Auto)
52+
}
53+
}
54+
55+
impl Liteval for Int<'_> {
56+
fn liteval(&self) -> EvalResult {
57+
Ok(Value::Int(self.get()))
58+
}
59+
}
60+
61+
impl Liteval for Parenthesized<'_> {
62+
fn liteval(&self) -> EvalResult {
63+
self.expr().liteval()
64+
}
65+
}
66+
67+
impl Liteval for Array<'_> {
68+
fn liteval(&self) -> EvalResult {
69+
Ok(Value::Array(self.items().count()))
70+
}
71+
}
72+
73+
impl Liteval for Unary<'_> {
74+
fn liteval(&self) -> EvalResult {
75+
let expr = self.expr().liteval()?;
76+
match self.op() {
77+
UnOp::Pos => match expr {
78+
Value::Int(i) => Ok(Value::Int(i)),
79+
_ => Err(EvalError::InvalidOperation),
80+
},
81+
UnOp::Neg => match expr {
82+
Value::Int(i) => Ok(Value::Int(-i)),
83+
_ => Err(EvalError::InvalidOperation),
84+
},
85+
UnOp::Not => Err(EvalError::NotSupported),
86+
}
87+
}
88+
}
89+
impl Liteval for Binary<'_> {
90+
fn liteval(&self) -> EvalResult {
91+
let lhs = self.lhs().liteval()?;
92+
let rhs = self.rhs().liteval()?;
93+
match self.op() {
94+
BinOp::Add => match (lhs, rhs) {
95+
(Value::Int(l), Value::Int(r)) => Ok(Value::Int(l + r)),
96+
(Value::Array(l), Value::Array(r)) => Ok(Value::Array(l + r)),
97+
_ => Err(EvalError::InvalidOperation),
98+
},
99+
BinOp::Sub => match (lhs, rhs) {
100+
(Value::Int(l), Value::Int(r)) => Ok(Value::Int(l - r)),
101+
_ => Err(EvalError::InvalidOperation),
102+
},
103+
BinOp::Mul => match (lhs, rhs) {
104+
(Value::Int(l), Value::Int(r)) => Ok(Value::Int(l * r)),
105+
(Value::Array(l), Value::Int(r)) if r >= 0 => Ok(Value::Array(l * r as usize)),
106+
(Value::Int(l), Value::Array(r)) if l >= 0 => Ok(Value::Array(l as usize * r)),
107+
_ => Err(EvalError::InvalidOperation),
108+
},
109+
BinOp::Div => match (lhs, rhs) {
110+
(Value::Int(l), Value::Int(r)) if r != 0 => Ok(Value::Int(l / r)),
111+
_ => Err(EvalError::InvalidOperation),
112+
},
113+
_ => Err(EvalError::NotSupported),
114+
}
115+
}
116+
}
117+
118+
#[cfg(test)]
119+
mod tests {
120+
use super::*;
121+
122+
fn test_liteval(code: &str, expected: Value) {
123+
let root = typst_syntax::parse_code(code);
124+
let expr = root.cast::<Code>().unwrap().exprs().next().unwrap();
125+
assert_eq!(expr.liteval(), Ok(expected), "expr: {expr:#?}");
126+
}
127+
128+
#[test]
129+
fn test_simple_expr() {
130+
use Value::*;
131+
132+
test_liteval("none", None);
133+
test_liteval("auto", Auto);
134+
test_liteval("0", Int(0));
135+
test_liteval("1 + 2", Int(3));
136+
test_liteval("1 * 2", Int(2));
137+
test_liteval("1 - 2", Int(-1));
138+
test_liteval("(1 + 2) * 3", Int(9));
139+
test_liteval("(1fr,)", Array(1));
140+
test_liteval("(1pt, 2em) * 3", Array(6));
141+
test_liteval("(1, 2) + (3, 4, 5)", Array(5));
142+
test_liteval("(1,) * 2 + 2 * (3, 4)", Array(6));
143+
test_liteval("((1,) * 2 + 2 * (3,)) * 4", Array(16));
144+
}
145+
}

crates/typstyle-core/src/pretty/table.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ fn is_table_formattable(func_call: FuncCall<'_>) -> bool {
9393
}
9494

9595
fn get_table_columns(func_call: FuncCall<'_>) -> Option<usize> {
96+
use crate::liteval::{Liteval, Value};
97+
9698
let Some(columns_expr) = func_call.args().items().find_map(|node| {
9799
if let Arg::Named(named) = node {
98100
if named.name().as_str() == "columns" {
@@ -107,10 +109,10 @@ fn get_table_columns(func_call: FuncCall<'_>) -> Option<usize> {
107109
Some(1) // if not `columns` is provided, regard as 1.
108110
};
109111
};
110-
match columns_expr {
111-
Expr::Auto(_) => Some(1),
112-
Expr::Int(int) => Some(int.get() as usize),
113-
Expr::Array(array) => Some(array.items().count()),
112+
match columns_expr.liteval() {
113+
Ok(Value::Auto) => Some(1),
114+
Ok(Value::Int(i)) => Some(i as usize),
115+
Ok(Value::Array(a)) => Some(a),
114116
_ => None,
115117
}
116118
}

docs/limitations.md

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# Limitations
2+
13
To ensure that source code remains valid, typstyle refrains from formatting in certain scenarios. The following cases outline situations where typstyle either does not apply formatting or applies only conservative changes.
24

35
## Overall
@@ -8,7 +10,8 @@ This directive explicitly disables formatting.
810

911
### Expressions with Comments
1012

11-
After months of work, we can now proudly format everything with comments!
13+
Currently, we are capable of formatting everything with comments.
14+
However, when expressions contain comments, typstyle may not be able to preserve a visually pleasing layout or may even refuse to format the code. Embedded comments can interrupt alignment and grouping, making it difficult to apply standard formatting rules without altering the intended structure. In such cases, typstyle falls back to conservative formatting or skips formatting entirely to avoid breaking the code’s semantics.
1215

1316
We guarantee that in all supported cases the source will be formatted correctly and no comments will be lost.
1417
If you find that a comment is lost or the formatting result is unsatisfactory due to comments, please submit a PR to present the issue.
@@ -23,12 +26,30 @@ Additionally, typstyle will not convert spaces into line breaks (or vice versa)
2326

2427
### Tables
2528

26-
Typstyle attempts to format tables into a neat, rectangular layout—but only when the table is simple enough. A table is considered "simple" if it meets all of the following conditions:
27-
28-
1. No comments.
29-
2. No spread args.
30-
3. No named args, or named args appears before all pos args.
31-
4. No `{table,grid}.{vline,hline,cell}`.
32-
5. `columns` is int or array.
33-
34-
Note that we can only recognize functions named `table` and `grid`, and `{table,grid}.{header,footer}` as args. Aliases or wrappers of `std.{table,grid}` are not supported.
29+
Typstyle attempts to format tables into neat, rectangular layouts—only when the table is simple enough.
30+
31+
Since there is no runtime function-signature provider, we treat any call named `table` or `grid` as a table and apply table layout.
32+
33+
We fall back to a plain layout (structure preserved) in these cases:
34+
35+
- The table contains a block comment or has no positional arguments.
36+
- It lacks a `columns` argument or uses spread arguments which possibly define columns.
37+
- The `columns` argument is not a simple constant expression. Allowed expressions are integer literals, array literals, or arithmetic combinations thereof (for example, `(auto,) * 2 + (1fr, 2fr)`).
38+
39+
#### Formatting Behavior
40+
41+
1. General Rules
42+
- `header`, `footer`, and line comments (`//`) always occupy their own lines.
43+
- Block comments disable table formatting entirely.
44+
- Blank lines are preserved and prevent reflow across them.
45+
2. Header & Footer
46+
- Both follow the table’s defined column layout.
47+
3. Cell Reflow
48+
- Reflow applies only when **no special cells** are present.
49+
Special cells include:
50+
- `cell`
51+
- `hline`
52+
- `vline`
53+
- Spread args (`..`)
54+
- If no special cells exist, typstyle reflows all cells to fit the columns.
55+
- Otherwise, the original grid structure is preserved.

0 commit comments

Comments
 (0)