feat: evaluate table and grid (#1300)

* feat: add table and grid functions evaluating

* fix: clippy

* refactor: simplify table and grid functions by extracting common logic

* fix: handle invalid columns argument and set default value; handle no header situation
This commit is contained in:
Hong Jiarong 2025-02-18 10:14:02 +08:00 committed by GitHub
parent cddb193b18
commit b24a7fd497
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -2,6 +2,7 @@
use super::*;
use ecow::eco_format;
use typst_syntax::{ast, SyntaxKind, SyntaxNode};
use value::*;
mod docstring;
@ -21,6 +22,8 @@ pub fn library() -> Scopes<Value> {
scopes.define("important-box", important_box as RawFunc);
scopes.define("warning-box", warning_box as RawFunc);
scopes.define("caution-box", caution_box as RawFunc);
scopes.define("table", table as RawFunc);
scopes.define("grid", grid as RawFunc);
scopes
}
@ -133,3 +136,99 @@ fn note_box(title: &str, body: Content) -> Value {
Value::Content(res)
}
/// Evaluate a table.
pub fn table(args: Args) -> Result<Value> {
table_eval(args, &EcoString::from("table"))
}
/// Evaluate a grid.
pub fn grid(args: Args) -> Result<Value> {
table_eval(args, &EcoString::from("grid"))
}
fn table_eval(mut args: Args, kind: &EcoString) -> Result<Value> {
let columns = if let Some(columns) = args.get_named_("columns") {
match columns.kind() {
SyntaxKind::Array => {
let array: ast::Array = args.get_named_("columns").unwrap().cast().unwrap();
array.items().count()
}
SyntaxKind::Int => {
let int_val: ast::Int = args.get_named_("columns").unwrap().cast().unwrap();
int_val.get().try_into().unwrap()
}
other => return Err(format!("invalid columns argument of type {:?}", other).into()),
}
} else {
1
};
let header_field = SyntaxNode::inner(
SyntaxKind::FieldAccess,
vec![
SyntaxNode::leaf(SyntaxKind::Ident, kind),
SyntaxNode::leaf(SyntaxKind::Dot, "."),
SyntaxNode::leaf(SyntaxKind::Ident, "header"),
],
);
let footer_field = SyntaxNode::inner(
SyntaxKind::FieldAccess,
vec![
SyntaxNode::leaf(SyntaxKind::Ident, kind),
SyntaxNode::leaf(SyntaxKind::Dot, "."),
SyntaxNode::leaf(SyntaxKind::Ident, "footer"),
],
);
let mut header: Vec<EcoString> = Vec::new();
let mut cells: Vec<EcoString> = Vec::new();
while let Some(pos_arg) = args.pos.pop() {
if pos_arg.kind() != SyntaxKind::FuncCall {
let evaluated = args.vm.eval(pos_arg)?;
cells.push(TypliteWorker::value(evaluated));
} else {
let func_call: ast::FuncCall = pos_arg.cast().unwrap();
let first_child = pos_arg.children().next().unwrap();
if header_field.spanless_eq(first_child) {
let mut header_args = Args::new(args.vm, func_call.args());
while let Some(arg) = header_args.pos.pop() {
let evaluated = header_args.vm.eval(arg)?;
header.push(TypliteWorker::value(evaluated));
}
} else {
let evaluated = args.vm.eval(pos_arg)?;
cells.push(TypliteWorker::value(evaluated));
}
if footer_field.spanless_eq(first_child) {
let mut footer_args = Args::new(args.vm, func_call.args());
while let Some(arg) = footer_args.pos.pop() {
let evaluated = footer_args.vm.eval(arg)?;
cells.push(TypliteWorker::value(evaluated));
}
}
}
}
let mut res = EcoString::from("<table>\n");
if !header.is_empty() {
res.push_str(" <thead>\n <tr>\n");
for cell in &header {
res.push_str(&eco_format!(" <th>{}</th>\n", cell));
}
res.push_str(" </tr>\n </thead>\n");
}
res.push_str(" <tbody>\n");
for row in cells.chunks(columns) {
res.push_str(" <tr>\n");
for cell in row {
res.push_str(&eco_format!(" <td>{}</td>\n", cell));
}
res.push_str(" </tr>\n");
}
res.push_str(" </tbody>\n</table>");
Ok(Value::Content(res))
}