1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
//! Cell types and alignment for LaTeX table generation
use typst_syntax::SyntaxNode;
use crate::core::typst2latex::context::{ConvertContext, EnvironmentContext};
use crate::core::typst2latex::markup::convert_markup_node;
use crate::core::typst2latex::utils::{
format_latex_color_command, normalize_typst_color_expr, FuncArgs,
};
/// LaTeX cell alignment options
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum LatexCellAlign {
Left,
#[default]
Center,
Right,
/// Paragraph column with width, e.g., p{3cm}
Para,
}
impl LatexCellAlign {
/// Convert to LaTeX column specification character
pub fn to_char(self) -> char {
match self {
LatexCellAlign::Left => 'l',
LatexCellAlign::Center => 'c',
LatexCellAlign::Right => 'r',
LatexCellAlign::Para => 'p',
}
}
/// Parse from Typst alignment string
pub fn from_typst(s: &str) -> Self {
match s.trim().to_lowercase().as_str() {
"left" => LatexCellAlign::Left,
"center" => LatexCellAlign::Center,
"right" => LatexCellAlign::Right,
_ => LatexCellAlign::Center,
}
}
}
/// Represents a single table cell with span and alignment info
#[derive(Debug, Clone)]
pub struct LatexCell {
/// Cell content (LaTeX code)
pub content: String,
/// Number of rows this cell spans
pub rowspan: usize,
/// Number of columns this cell spans
pub colspan: usize,
/// Optional cell-specific alignment
pub align: Option<LatexCellAlign>,
/// Optional cell background color
pub fill: Option<String>,
/// Whether this is a header cell
pub is_header: bool,
/// Whether this is an empty placeholder (for rowspan coverage)
pub is_placeholder: bool,
}
impl LatexCell {
/// Create a new cell with content
pub fn new(content: String) -> Self {
LatexCell {
content,
rowspan: 1,
colspan: 1,
align: None,
fill: None,
is_header: false,
is_placeholder: false,
}
}
/// Create an empty placeholder cell (for rowspan coverage)
pub fn placeholder() -> Self {
LatexCell {
content: String::new(),
rowspan: 1,
colspan: 1,
align: None,
fill: None,
is_header: false,
is_placeholder: true,
}
}
/// Create a cell with all attributes
#[allow(dead_code)]
pub fn with_spans(content: String, rowspan: usize, colspan: usize) -> Self {
LatexCell {
content,
rowspan,
colspan,
align: None,
fill: None,
is_header: false,
is_placeholder: false,
}
}
/// Parse a table.cell(...) FuncCall node from Typst AST
pub fn from_typst_cell_ast(node: &SyntaxNode, ctx: &mut ConvertContext) -> Self {
use typst_syntax::SyntaxKind;
let mut content = String::new();
let mut colspan = 1usize;
let mut rowspan = 1usize;
let mut align = None;
let mut fill = None;
let children: Vec<_> = node.children().collect();
let args = FuncArgs::from_func_call(&children);
for child in node.children() {
if child.kind() == SyntaxKind::Args {
for arg in child.children() {
match arg.kind() {
SyntaxKind::Named => {
if let Some(parsed) = args.arg_for_node(arg) {
match parsed.name.as_deref() {
Some("colspan") => {
if let Some(n) = args.named_usize("colspan") {
colspan = n;
}
}
Some("rowspan") => {
if let Some(n) = args.named_usize("rowspan") {
rowspan = n;
}
}
Some("align") => {
align = Some(LatexCellAlign::from_typst(&parsed.value));
}
Some("fill") => {
fill = normalize_typst_color_expr(&parsed.value);
}
_ => {}
}
}
}
SyntaxKind::ContentBlock => {
// Cell content [...]
let mut cell_ctx = ConvertContext::new();
cell_ctx.push_env(EnvironmentContext::Table);
convert_markup_node(arg, &mut cell_ctx);
content = cell_ctx.finalize();
}
SyntaxKind::FuncCall => {
// Cell content can be a function call like text(...)[...]
let mut cell_ctx = ConvertContext::new();
cell_ctx.push_env(EnvironmentContext::Table);
convert_markup_node(arg, &mut cell_ctx);
let func_content = cell_ctx.finalize();
if !func_content.is_empty() {
content = func_content;
}
}
_ => {}
}
}
}
}
// If no ContentBlock was found, try to get content from trailing content
if content.is_empty() {
let mut cell_ctx = ConvertContext::new();
cell_ctx.push_env(EnvironmentContext::Table);
for child in node.children() {
if child.kind() == SyntaxKind::ContentBlock {
convert_markup_node(child, &mut cell_ctx);
}
}
content = cell_ctx.finalize();
}
// Use the passed context for any additional processing
let _ = ctx;
LatexCell {
content,
rowspan,
colspan,
align,
fill,
is_header: false,
is_placeholder: false,
}
}
/// Generate LaTeX code for this cell
pub fn to_latex(&self, default_align: LatexCellAlign) -> String {
if self.is_placeholder {
return String::new();
}
let content_str = self.content.trim();
let mut prefix = String::new();
// Add cell color if present
if let Some(ref color) = self.fill {
prefix.push_str(&format!(
"{} ",
format_latex_color_command("cellcolor", color)
));
}
let content = format!("{}{}", prefix, content_str);
// Build the inner content (potentially wrapped in \multirow)
let inner = if self.rowspan > 1 {
format!("\\multirow{{{}}}{{*}}{{{}}}", self.rowspan, content)
} else {
content
};
// Wrap in \multicolumn if needed
if self.colspan > 1 {
let align_char = self.align.unwrap_or(default_align).to_char();
format!(
"\\multicolumn{{{}}}{{|{}|}}{{{}}}",
self.colspan, align_char, inner
)
} else {
inner
}
}
}