mirror of
https://github.com/duhanbalci/dreport.git
synced 2026-07-01 18:39:16 +00:00
fixes
This commit is contained in:
@@ -9,6 +9,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
dreport-core = { path = "../core" }
|
||||
dexpr = { path = "../../rust-expr" }
|
||||
taffy = "0.9"
|
||||
cosmic-text = { version = "0.18", default-features = false, features = ["std", "swash"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
@@ -18,6 +19,8 @@ rxing = { version = "0.8", default-features = false, features = ["encoding_rs"]
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
js-sys = "0.3"
|
||||
getrandom_03 = { package = "getrandom", version = "0.3", features = ["wasm_js"] }
|
||||
getrandom_04 = { package = "getrandom", version = "0.4", features = ["wasm_js"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
krilla = { version = "0.6", features = ["raster-images", "simple-text"] }
|
||||
|
||||
@@ -219,7 +219,9 @@ fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedDa
|
||||
TemplateElement::CalculatedText(e) => {
|
||||
let result = crate::expr_eval::evaluate_expression(&e.expression, data);
|
||||
let formatted = crate::expr_eval::apply_format(&result, e.format.as_deref());
|
||||
resolved.texts.insert(e.id.clone(), formatted);
|
||||
// Bos ifade veya hata durumunda placeholder goster — element 0 yukseklige dusmesin
|
||||
let text = if formatted.is_empty() { " ".to_string() } else { formatted };
|
||||
resolved.texts.insert(e.id.clone(), text);
|
||||
}
|
||||
TemplateElement::RichText(e) => {
|
||||
let spans: Vec<ResolvedRichSpan> = e
|
||||
|
||||
@@ -1,42 +1,67 @@
|
||||
use dexpr::ast::value::Value as DexprValue;
|
||||
use dexpr::compiler::Compiler;
|
||||
use dexpr::vm::VM;
|
||||
use serde_json::Value;
|
||||
|
||||
/// Expression evaluator for calculated_text elements.
|
||||
/// This is a safe recursive descent parser — NOT an arbitrary code executor.
|
||||
/// It only supports arithmetic, string operations, comparisons, and data path lookups.
|
||||
/// Expression evaluator for calculated_text elements using dexpr engine.
|
||||
/// Supports arithmetic, string ops, comparisons, conditionals, methods, and more.
|
||||
///
|
||||
/// Supported syntax:
|
||||
/// - Path lookup: `firma.unvan`, `toplamlar.kdv`
|
||||
/// - Arithmetic: `+`, `-`, `*`, `/`
|
||||
/// - String concatenation: `+` when operand is string
|
||||
/// - String literals: `"..."` or `'...'`
|
||||
/// - Number literals: `42`, `3.14`
|
||||
/// - Comparison: `>`, `<`, `>=`, `<=`, `==`, `!=`
|
||||
/// - Ternary: `expr ? "a" : "b"`
|
||||
/// - Parentheses: `(a + b) * c`
|
||||
|
||||
/// Data JSON's top-level keys are set as global variables in dexpr.
|
||||
/// Expressions like `firma.unvan` or `toplamlar.kdv + toplamlar.araToplam` work directly.
|
||||
pub fn evaluate_expression(expr: &str, data: &Value) -> String {
|
||||
let tokens = tokenize(expr);
|
||||
if tokens.is_empty() {
|
||||
if expr.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
let mut parser = Parser {
|
||||
tokens: &tokens,
|
||||
pos: 0,
|
||||
data,
|
||||
|
||||
let mut compiler = Compiler::new();
|
||||
let bytecode = match compiler.compile_from_source(expr) {
|
||||
Ok((bc, _)) => bc,
|
||||
Err(_) => return String::new(),
|
||||
};
|
||||
match parser.parse_ternary() {
|
||||
ExprValue::Num(n) => format_number(n),
|
||||
ExprValue::Str(s) => s,
|
||||
ExprValue::Bool(b) => b.to_string(),
|
||||
ExprValue::Null => String::new(),
|
||||
|
||||
let mut vm = VM::new(&bytecode);
|
||||
|
||||
// Set each top-level key in data as a dexpr global
|
||||
if let Value::Object(map) = data {
|
||||
for (key, val) in map {
|
||||
if let Ok(dval) = DexprValue::from_json_value(val) {
|
||||
vm.set_global(key, dval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match vm.execute() {
|
||||
Ok(result) => dexpr_value_to_string(&result),
|
||||
Err(_) => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_number(n: f64) -> String {
|
||||
if n == n.floor() && n.abs() < 1e15 {
|
||||
format!("{}", n as i64)
|
||||
} else {
|
||||
format!("{}", n)
|
||||
/// Convert dexpr Value to display string
|
||||
fn dexpr_value_to_string(val: &DexprValue) -> String {
|
||||
match val {
|
||||
DexprValue::Null => String::new(),
|
||||
DexprValue::Boolean(b) => b.to_string(),
|
||||
DexprValue::Number(n) => {
|
||||
// Format: no trailing zeros for integers
|
||||
if n.scale() == 0 {
|
||||
n.to_string()
|
||||
} else {
|
||||
n.normalize().to_string()
|
||||
}
|
||||
}
|
||||
DexprValue::String(s) => s.to_string(),
|
||||
DexprValue::NumberList(list) => {
|
||||
let items: Vec<String> = list.iter().map(|n| n.to_string()).collect();
|
||||
format!("[{}]", items.join(", "))
|
||||
}
|
||||
DexprValue::StringList(list) => {
|
||||
let items: Vec<String> = list.iter().map(|s| s.to_string()).collect();
|
||||
format!("[{}]", items.join(", "))
|
||||
}
|
||||
DexprValue::Object(map) => {
|
||||
let items: Vec<String> = map.iter().map(|(k, v)| format!("{}: {}", k, dexpr_value_to_string(v))).collect();
|
||||
format!("{{{}}}", items.join(", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,319 +125,6 @@ fn format_with_thousands(n: i64) -> String {
|
||||
result
|
||||
}
|
||||
|
||||
// --- Tokenizer ---
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum Token {
|
||||
Num(f64),
|
||||
Str(String),
|
||||
Ident(String),
|
||||
Plus,
|
||||
Minus,
|
||||
Star,
|
||||
Slash,
|
||||
LParen,
|
||||
RParen,
|
||||
Gt,
|
||||
Lt,
|
||||
Gte,
|
||||
Lte,
|
||||
Eq,
|
||||
Neq,
|
||||
Question,
|
||||
Colon,
|
||||
}
|
||||
|
||||
fn tokenize(input: &str) -> Vec<Token> {
|
||||
let mut tokens = Vec::new();
|
||||
let chars: Vec<char> = input.chars().collect();
|
||||
let len = chars.len();
|
||||
let mut i = 0;
|
||||
|
||||
while i < len {
|
||||
match chars[i] {
|
||||
' ' | '\t' | '\n' | '\r' => i += 1,
|
||||
'+' => { tokens.push(Token::Plus); i += 1; }
|
||||
'-' => {
|
||||
// Negative number: after operator or at start
|
||||
let is_unary = tokens.is_empty()
|
||||
|| matches!(tokens.last(), Some(
|
||||
Token::Plus | Token::Minus | Token::Star | Token::Slash
|
||||
| Token::LParen | Token::Question | Token::Colon
|
||||
| Token::Gt | Token::Lt | Token::Gte | Token::Lte
|
||||
| Token::Eq | Token::Neq
|
||||
));
|
||||
if is_unary && i + 1 < len && (chars[i + 1].is_ascii_digit() || chars[i + 1] == '.') {
|
||||
let start = i;
|
||||
i += 1;
|
||||
while i < len && (chars[i].is_ascii_digit() || chars[i] == '.') {
|
||||
i += 1;
|
||||
}
|
||||
let num_str: String = chars[start..i].iter().collect();
|
||||
if let Ok(n) = num_str.parse::<f64>() {
|
||||
tokens.push(Token::Num(n));
|
||||
}
|
||||
} else {
|
||||
tokens.push(Token::Minus);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
'*' => { tokens.push(Token::Star); i += 1; }
|
||||
'/' => { tokens.push(Token::Slash); i += 1; }
|
||||
'(' => { tokens.push(Token::LParen); i += 1; }
|
||||
')' => { tokens.push(Token::RParen); i += 1; }
|
||||
'?' => { tokens.push(Token::Question); i += 1; }
|
||||
':' => { tokens.push(Token::Colon); i += 1; }
|
||||
'>' => {
|
||||
if i + 1 < len && chars[i + 1] == '=' {
|
||||
tokens.push(Token::Gte); i += 2;
|
||||
} else {
|
||||
tokens.push(Token::Gt); i += 1;
|
||||
}
|
||||
}
|
||||
'<' => {
|
||||
if i + 1 < len && chars[i + 1] == '=' {
|
||||
tokens.push(Token::Lte); i += 2;
|
||||
} else {
|
||||
tokens.push(Token::Lt); i += 1;
|
||||
}
|
||||
}
|
||||
'=' => {
|
||||
if i + 1 < len && chars[i + 1] == '=' {
|
||||
tokens.push(Token::Eq); i += 2;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
'!' => {
|
||||
if i + 1 < len && chars[i + 1] == '=' {
|
||||
tokens.push(Token::Neq); i += 2;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
'"' | '\'' => {
|
||||
let quote = chars[i];
|
||||
i += 1;
|
||||
let start = i;
|
||||
while i < len && chars[i] != quote {
|
||||
i += 1;
|
||||
}
|
||||
let s: String = chars[start..i].iter().collect();
|
||||
tokens.push(Token::Str(s));
|
||||
if i < len { i += 1; }
|
||||
}
|
||||
c if c.is_ascii_digit() || (c == '.' && i + 1 < len && chars[i + 1].is_ascii_digit()) => {
|
||||
let start = i;
|
||||
while i < len && (chars[i].is_ascii_digit() || chars[i] == '.') {
|
||||
i += 1;
|
||||
}
|
||||
let num_str: String = chars[start..i].iter().collect();
|
||||
if let Ok(n) = num_str.parse::<f64>() {
|
||||
tokens.push(Token::Num(n));
|
||||
}
|
||||
}
|
||||
c if c.is_alphanumeric() || c == '_' => {
|
||||
let start = i;
|
||||
while i < len && (chars[i].is_alphanumeric() || chars[i] == '_' || chars[i] == '.') {
|
||||
i += 1;
|
||||
}
|
||||
// Trim trailing dots
|
||||
while i > start && chars[i - 1] == '.' {
|
||||
i -= 1;
|
||||
}
|
||||
let ident: String = chars[start..i].iter().collect();
|
||||
match ident.as_str() {
|
||||
"true" => tokens.push(Token::Num(1.0)),
|
||||
"false" => tokens.push(Token::Num(0.0)),
|
||||
_ => tokens.push(Token::Ident(ident)),
|
||||
}
|
||||
}
|
||||
_ => i += 1,
|
||||
}
|
||||
}
|
||||
|
||||
tokens
|
||||
}
|
||||
|
||||
// --- Parser (recursive descent) ---
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum ExprValue {
|
||||
Num(f64),
|
||||
Str(String),
|
||||
Bool(bool),
|
||||
Null,
|
||||
}
|
||||
|
||||
impl ExprValue {
|
||||
fn to_num(&self) -> f64 {
|
||||
match self {
|
||||
ExprValue::Num(n) => *n,
|
||||
ExprValue::Str(s) => s.parse().unwrap_or(0.0),
|
||||
ExprValue::Bool(b) => if *b { 1.0 } else { 0.0 },
|
||||
ExprValue::Null => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_str(&self) -> String {
|
||||
match self {
|
||||
ExprValue::Num(n) => format_number(*n),
|
||||
ExprValue::Str(s) => s.clone(),
|
||||
ExprValue::Bool(b) => b.to_string(),
|
||||
ExprValue::Null => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_truthy(&self) -> bool {
|
||||
match self {
|
||||
ExprValue::Num(n) => *n != 0.0,
|
||||
ExprValue::Str(s) => !s.is_empty(),
|
||||
ExprValue::Bool(b) => *b,
|
||||
ExprValue::Null => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_string(&self) -> bool {
|
||||
matches!(self, ExprValue::Str(_))
|
||||
}
|
||||
}
|
||||
|
||||
struct Parser<'a> {
|
||||
tokens: &'a [Token],
|
||||
pos: usize,
|
||||
data: &'a Value,
|
||||
}
|
||||
|
||||
impl<'a> Parser<'a> {
|
||||
fn peek(&self) -> Option<&Token> {
|
||||
self.tokens.get(self.pos)
|
||||
}
|
||||
|
||||
fn advance(&mut self) -> Option<&Token> {
|
||||
let tok = self.tokens.get(self.pos);
|
||||
self.pos += 1;
|
||||
tok
|
||||
}
|
||||
|
||||
fn parse_ternary(&mut self) -> ExprValue {
|
||||
let cond = self.parse_comparison();
|
||||
if self.peek() == Some(&Token::Question) {
|
||||
self.advance();
|
||||
let then_val = self.parse_ternary();
|
||||
if self.peek() == Some(&Token::Colon) {
|
||||
self.advance();
|
||||
}
|
||||
let else_val = self.parse_ternary();
|
||||
if cond.is_truthy() { then_val } else { else_val }
|
||||
} else {
|
||||
cond
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_comparison(&mut self) -> ExprValue {
|
||||
let left = self.parse_additive();
|
||||
match self.peek() {
|
||||
Some(Token::Gt) => { self.advance(); let r = self.parse_additive(); ExprValue::Bool(left.to_num() > r.to_num()) }
|
||||
Some(Token::Lt) => { self.advance(); let r = self.parse_additive(); ExprValue::Bool(left.to_num() < r.to_num()) }
|
||||
Some(Token::Gte) => { self.advance(); let r = self.parse_additive(); ExprValue::Bool(left.to_num() >= r.to_num()) }
|
||||
Some(Token::Lte) => { self.advance(); let r = self.parse_additive(); ExprValue::Bool(left.to_num() <= r.to_num()) }
|
||||
Some(Token::Eq) => { self.advance(); let r = self.parse_additive(); ExprValue::Bool(left.to_str() == r.to_str()) }
|
||||
Some(Token::Neq) => { self.advance(); let r = self.parse_additive(); ExprValue::Bool(left.to_str() != r.to_str()) }
|
||||
_ => left,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_additive(&mut self) -> ExprValue {
|
||||
let mut left = self.parse_multiplicative();
|
||||
loop {
|
||||
match self.peek() {
|
||||
Some(Token::Plus) => {
|
||||
self.advance();
|
||||
let right = self.parse_multiplicative();
|
||||
if left.is_string() || right.is_string() {
|
||||
left = ExprValue::Str(format!("{}{}", left.to_str(), right.to_str()));
|
||||
} else {
|
||||
left = ExprValue::Num(left.to_num() + right.to_num());
|
||||
}
|
||||
}
|
||||
Some(Token::Minus) => {
|
||||
self.advance();
|
||||
let right = self.parse_multiplicative();
|
||||
left = ExprValue::Num(left.to_num() - right.to_num());
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
left
|
||||
}
|
||||
|
||||
fn parse_multiplicative(&mut self) -> ExprValue {
|
||||
let mut left = self.parse_primary();
|
||||
loop {
|
||||
match self.peek() {
|
||||
Some(Token::Star) => {
|
||||
self.advance();
|
||||
let right = self.parse_primary();
|
||||
left = ExprValue::Num(left.to_num() * right.to_num());
|
||||
}
|
||||
Some(Token::Slash) => {
|
||||
self.advance();
|
||||
let right = self.parse_primary();
|
||||
let r = right.to_num();
|
||||
left = ExprValue::Num(if r != 0.0 { left.to_num() / r } else { 0.0 });
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
left
|
||||
}
|
||||
|
||||
fn parse_primary(&mut self) -> ExprValue {
|
||||
match self.advance().cloned() {
|
||||
Some(Token::Num(n)) => ExprValue::Num(n),
|
||||
Some(Token::Str(s)) => ExprValue::Str(s),
|
||||
Some(Token::Ident(path)) => {
|
||||
let val = resolve_path(self.data, &path);
|
||||
json_to_expr(val)
|
||||
}
|
||||
Some(Token::LParen) => {
|
||||
let val = self.parse_ternary();
|
||||
if self.peek() == Some(&Token::RParen) {
|
||||
self.advance();
|
||||
}
|
||||
val
|
||||
}
|
||||
Some(Token::Minus) => {
|
||||
let val = self.parse_primary();
|
||||
ExprValue::Num(-val.to_num())
|
||||
}
|
||||
_ => ExprValue::Null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_path<'a>(data: &'a Value, path: &str) -> &'a Value {
|
||||
let mut current = data;
|
||||
for key in path.split('.') {
|
||||
current = match current {
|
||||
Value::Object(map) => map.get(key).unwrap_or(&Value::Null),
|
||||
_ => &Value::Null,
|
||||
};
|
||||
}
|
||||
current
|
||||
}
|
||||
|
||||
fn json_to_expr(v: &Value) -> ExprValue {
|
||||
match v {
|
||||
Value::Number(n) => ExprValue::Num(n.as_f64().unwrap_or(0.0)),
|
||||
Value::String(s) => ExprValue::Str(s.clone()),
|
||||
Value::Bool(b) => ExprValue::Bool(*b),
|
||||
Value::Null => ExprValue::Null,
|
||||
_ => ExprValue::Str(v.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -445,13 +157,19 @@ mod tests {
|
||||
#[test]
|
||||
fn test_ternary() {
|
||||
let data = json!({"fatura": {"tutar": 5000}});
|
||||
assert_eq!(evaluate_expression("fatura.tutar > 0 ? \"Borclu\" : \"Alacakli\"", &data), "Borclu");
|
||||
assert_eq!(
|
||||
evaluate_expression("if fatura.tutar > 0 then \"Borclu\" else \"Alacakli\" end", &data),
|
||||
"Borclu"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ternary_false() {
|
||||
let data = json!({"fatura": {"tutar": 0}});
|
||||
assert_eq!(evaluate_expression("fatura.tutar > 0 ? \"Borclu\" : \"Alacakli\"", &data), "Alacakli");
|
||||
assert_eq!(
|
||||
evaluate_expression("if fatura.tutar > 0 then \"Borclu\" else \"Alacakli\" end", &data),
|
||||
"Alacakli"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -467,22 +185,20 @@ mod tests {
|
||||
assert_eq!(evaluate_expression("3.14", &data), "3.14");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_division_by_zero() {
|
||||
let data = json!({});
|
||||
assert_eq!(evaluate_expression("10 / 0", &data), "0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_path() {
|
||||
let data = json!({});
|
||||
// dexpr returns Null for undefined globals
|
||||
assert_eq!(evaluate_expression("missing.path", &data), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_comparison_eq() {
|
||||
let data = json!({"status": "paid"});
|
||||
assert_eq!(evaluate_expression("status == \"paid\" ? \"Odendi\" : \"Odenmedi\"", &data), "Odendi");
|
||||
fn test_numeric_comparison() {
|
||||
let data = json!({"fatura": {"tutar": 5000}});
|
||||
assert_eq!(
|
||||
evaluate_expression("if fatura.tutar > 1000 then \"Yuksek\" else \"Dusuk\" end", &data),
|
||||
"Yuksek"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -507,4 +223,35 @@ mod tests {
|
||||
let data = json!({});
|
||||
assert_eq!(evaluate_expression("", &data), "");
|
||||
}
|
||||
|
||||
// dexpr-specific features
|
||||
#[test]
|
||||
fn test_string_methods() {
|
||||
let data = json!({"name": "Acme Teknoloji"});
|
||||
assert_eq!(evaluate_expression("name.upper()", &data), "ACME TEKNOLOJI");
|
||||
assert_eq!(evaluate_expression("name.length()", &data), "14");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modulo_and_power() {
|
||||
let data = json!({});
|
||||
assert_eq!(evaluate_expression("10 % 3", &data), "1");
|
||||
assert_eq!(evaluate_expression("2 ** 10", &data), "1024");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_logical_operators() {
|
||||
let data = json!({"a": true, "b": false});
|
||||
assert_eq!(evaluate_expression("a && b", &data), "false");
|
||||
assert_eq!(evaluate_expression("a || b", &data), "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compound_expression() {
|
||||
let data = json!({"toplamlar": {"araToplam": 16000, "kdvOran": 18}});
|
||||
assert_eq!(
|
||||
evaluate_expression("toplamlar.araToplam * toplamlar.kdvOran / 100", &data),
|
||||
"2880"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,10 @@ pub enum ResolvedContent {
|
||||
#[serde(rename = "page_number")]
|
||||
PageNumber { current: usize, total: usize },
|
||||
#[serde(rename = "shape")]
|
||||
Shape { shape_type: String },
|
||||
Shape {
|
||||
#[serde(rename = "shapeType")]
|
||||
shape_type: String,
|
||||
},
|
||||
#[serde(rename = "checkbox")]
|
||||
Checkbox { checked: bool },
|
||||
#[serde(rename = "rich_text")]
|
||||
|
||||
@@ -1,6 +1,142 @@
|
||||
use dreport_core::models::*;
|
||||
|
||||
use crate::data_resolve::ResolvedData;
|
||||
use crate::text_measure::TextMeasurer;
|
||||
|
||||
/// Her auto sütun için header + tüm data satırlarındaki en geniş text'i ölç,
|
||||
/// doğal genişliklerini Fixed olarak ata.
|
||||
/// Fr sütunları olduğu gibi bırak (kalan alanı taffy dağıtır).
|
||||
/// Sadece auto sütunlar varsa (fr/fixed yoksa) kalan alanı oransal dağıt.
|
||||
fn compute_auto_column_widths(
|
||||
table: &RepeatingTableElement,
|
||||
rows: &[Vec<String>],
|
||||
measurer: &mut TextMeasurer,
|
||||
available_width_mm: f64,
|
||||
) -> Vec<SizeValue> {
|
||||
let num_cols = table.columns.len();
|
||||
let font_size = table.style.font_size.unwrap_or(10.0);
|
||||
let header_font_size = table.style.header_font_size.unwrap_or(font_size);
|
||||
let cell_pad_h = table.style.cell_padding_h.unwrap_or(2.0);
|
||||
let header_pad_h = table.style.header_padding_h.unwrap_or(cell_pad_h);
|
||||
// Ölçüme dahil edilecek max yatay padding (header ve cell'den büyük olanı)
|
||||
let max_pad_h = cell_pad_h.max(header_pad_h);
|
||||
|
||||
// Hangi sütunlar auto?
|
||||
let is_auto: Vec<bool> = table.columns.iter().map(|c| matches!(c.width, SizeValue::Auto)).collect();
|
||||
|
||||
// Hiç auto yoksa olduğu gibi dön
|
||||
if !is_auto.iter().any(|&a| a) {
|
||||
return table.columns.iter().map(|c| c.width.clone()).collect();
|
||||
}
|
||||
|
||||
// Fr sütun var mı?
|
||||
let has_fr = table.columns.iter().any(|c| matches!(c.width, SizeValue::Fr { .. }));
|
||||
|
||||
// Her auto sütun için max içerik genişliğini ölç (mm cinsinden)
|
||||
let mut max_widths_mm = vec![0.0_f64; num_cols];
|
||||
|
||||
for (col_idx, col) in table.columns.iter().enumerate() {
|
||||
if !is_auto[col_idx] {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Header text ölçümü (font_size zaten pt cinsinden)
|
||||
let (header_w_pt, _) = measurer.measure(
|
||||
&col.title,
|
||||
None,
|
||||
header_font_size as f32,
|
||||
Some("bold"),
|
||||
None,
|
||||
);
|
||||
let header_w_mm = header_w_pt as f64 / (72.0 / 25.4);
|
||||
max_widths_mm[col_idx] = header_w_mm;
|
||||
|
||||
// Data row text ölçümü
|
||||
for row in rows {
|
||||
let text = row.get(col_idx).map(|s| s.as_str()).unwrap_or("");
|
||||
if text.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let (w_pt, _) = measurer.measure(
|
||||
text,
|
||||
None,
|
||||
font_size as f32,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let w_mm = w_pt as f64 / (72.0 / 25.4);
|
||||
if w_mm > max_widths_mm[col_idx] {
|
||||
max_widths_mm[col_idx] = w_mm;
|
||||
}
|
||||
}
|
||||
|
||||
// Yatay padding ekle (sol + sağ)
|
||||
max_widths_mm[col_idx] += max_pad_h * 2.0;
|
||||
}
|
||||
|
||||
// Fixed sütunların kapladığı alanı hesapla
|
||||
let mut fixed_total_mm = 0.0_f64;
|
||||
for (col_idx, col) in table.columns.iter().enumerate() {
|
||||
if !is_auto[col_idx] {
|
||||
if let SizeValue::Fixed { value } = &col.width {
|
||||
fixed_total_mm += value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto sütunların toplam doğal genişliği
|
||||
let auto_natural_total: f64 = max_widths_mm.iter().sum();
|
||||
let remaining_mm = available_width_mm - fixed_total_mm;
|
||||
|
||||
// Sonuç genişlikleri
|
||||
let mut result: Vec<SizeValue> = Vec::with_capacity(num_cols);
|
||||
|
||||
if has_fr {
|
||||
// Fr sütunlar var — auto sütunlara doğal genişliklerini ver,
|
||||
// kalan alanı Fr sütunlarına bırak (taffy flex ile dağıtır).
|
||||
|
||||
// Fr sütunları için minimum alan ayır (en az padding kadar)
|
||||
let fr_count = table.columns.iter()
|
||||
.filter(|c| matches!(c.width, SizeValue::Fr { .. }))
|
||||
.count();
|
||||
let fr_min_space = fr_count as f64 * max_pad_h * 2.0;
|
||||
let auto_budget = (remaining_mm - fr_min_space).max(0.0);
|
||||
|
||||
for (col_idx, col) in table.columns.iter().enumerate() {
|
||||
if !is_auto[col_idx] {
|
||||
result.push(col.width.clone());
|
||||
} else if auto_natural_total <= auto_budget {
|
||||
// Sığıyor — doğal genişliği kullan
|
||||
result.push(SizeValue::Fixed { value: max_widths_mm[col_idx] });
|
||||
} else if auto_budget > 0.0 && auto_natural_total > 0.0 {
|
||||
// Sığmıyor — budget'a oransal küçült
|
||||
let ratio = max_widths_mm[col_idx] / auto_natural_total;
|
||||
let width_mm = auto_budget * ratio;
|
||||
result.push(SizeValue::Fixed { value: width_mm });
|
||||
} else {
|
||||
result.push(SizeValue::Fixed { value: max_widths_mm[col_idx] });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fr sütun yok — kalan alanı auto sütunlar arasında oransal dağıt
|
||||
for (col_idx, col) in table.columns.iter().enumerate() {
|
||||
if !is_auto[col_idx] {
|
||||
result.push(col.width.clone());
|
||||
} else if auto_natural_total > 0.0 {
|
||||
let ratio = max_widths_mm[col_idx] / auto_natural_total;
|
||||
let width_mm = remaining_mm * ratio;
|
||||
result.push(SizeValue::Fixed { value: width_mm });
|
||||
} else {
|
||||
// Tüm auto sütunlar boş — eşit dağıt
|
||||
let auto_count = is_auto.iter().filter(|&&a| a).count();
|
||||
let width_mm = remaining_mm / auto_count as f64;
|
||||
result.push(SizeValue::Fixed { value: width_mm });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// RepeatingTable element'ini bir container ağacına expand eder.
|
||||
/// Tablo → column container (header row + data rows)
|
||||
@@ -10,25 +146,36 @@ use crate::data_resolve::ResolvedData;
|
||||
pub fn expand_table(
|
||||
table: &RepeatingTableElement,
|
||||
resolved: &ResolvedData,
|
||||
measurer: &mut TextMeasurer,
|
||||
available_width_mm: f64,
|
||||
) -> ContainerElement {
|
||||
let resolved_table = resolved.tables.get(&table.id);
|
||||
let rows = resolved_table
|
||||
.map(|t| t.rows.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
|
||||
// Auto sütunlar için içerik bazlı genişlik hesapla
|
||||
let effective_widths = compute_auto_column_widths(table, rows, measurer, available_width_mm);
|
||||
|
||||
// Padding değerleri (mm)
|
||||
let cell_pad_h = table.style.cell_padding_h.unwrap_or(2.0);
|
||||
let cell_pad_v = table.style.cell_padding_v.unwrap_or(1.0);
|
||||
let header_pad_h = table.style.header_padding_h.unwrap_or(cell_pad_h);
|
||||
let header_pad_v = table.style.header_padding_v.unwrap_or(cell_pad_v);
|
||||
|
||||
let mut children: Vec<TemplateElement> = Vec::new();
|
||||
|
||||
// Header row
|
||||
// Header row — her hücre padding container'ı içinde
|
||||
let header_cells: Vec<TemplateElement> = table
|
||||
.columns
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, col)| {
|
||||
TemplateElement::StaticText(StaticTextElement {
|
||||
let text = TemplateElement::StaticText(StaticTextElement {
|
||||
id: format!("{}_hdr_{}", table.id, i),
|
||||
position: PositionMode::Flow,
|
||||
size: SizeConstraint {
|
||||
width: col.width.clone(),
|
||||
width: SizeValue::Fr { value: 1.0 },
|
||||
height: SizeValue::Auto,
|
||||
min_width: None,
|
||||
min_height: None,
|
||||
@@ -43,6 +190,31 @@ pub fn expand_table(
|
||||
align: Some(col.align.clone()),
|
||||
},
|
||||
content: col.title.clone(),
|
||||
});
|
||||
TemplateElement::Container(ContainerElement {
|
||||
id: format!("{}_hdr_{}_wrap", table.id, i),
|
||||
position: PositionMode::Flow,
|
||||
size: SizeConstraint {
|
||||
width: effective_widths[i].clone(),
|
||||
height: SizeValue::Auto,
|
||||
min_width: None,
|
||||
min_height: None,
|
||||
max_width: None,
|
||||
max_height: None,
|
||||
},
|
||||
direction: "column".to_string(),
|
||||
gap: 0.0,
|
||||
padding: Padding {
|
||||
top: header_pad_v,
|
||||
right: header_pad_h,
|
||||
bottom: header_pad_v,
|
||||
left: header_pad_h,
|
||||
},
|
||||
align: "stretch".to_string(),
|
||||
justify: "start".to_string(),
|
||||
style: ContainerStyle::default(),
|
||||
children: vec![text],
|
||||
break_inside: "auto".to_string(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -61,12 +233,12 @@ pub fn expand_table(
|
||||
direction: "row".to_string(),
|
||||
gap: 0.0,
|
||||
padding: Padding {
|
||||
top: 1.0,
|
||||
top: 0.0,
|
||||
right: 0.0,
|
||||
bottom: 1.0,
|
||||
bottom: 0.0,
|
||||
left: 0.0,
|
||||
},
|
||||
align: "center".to_string(),
|
||||
align: "stretch".to_string(),
|
||||
justify: "start".to_string(),
|
||||
style: ContainerStyle {
|
||||
background_color: table.style.header_bg.clone(),
|
||||
@@ -96,23 +268,23 @@ pub fn expand_table(
|
||||
}));
|
||||
}
|
||||
|
||||
// Data rows
|
||||
// Data rows — her hücre padding container'ı içinde
|
||||
for (row_idx, row_data) in rows.iter().enumerate() {
|
||||
let cells: Vec<TemplateElement> = table
|
||||
.columns
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(col_idx, col)| {
|
||||
let text = row_data
|
||||
let text_content = row_data
|
||||
.get(col_idx)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
TemplateElement::StaticText(StaticTextElement {
|
||||
let text = TemplateElement::StaticText(StaticTextElement {
|
||||
id: format!("{}_r{}c{}", table.id, row_idx, col_idx),
|
||||
position: PositionMode::Flow,
|
||||
size: SizeConstraint {
|
||||
width: col.width.clone(),
|
||||
width: SizeValue::Fr { value: 1.0 },
|
||||
height: SizeValue::Auto,
|
||||
min_width: None,
|
||||
min_height: None,
|
||||
@@ -126,13 +298,38 @@ pub fn expand_table(
|
||||
color: None,
|
||||
align: Some(col.align.clone()),
|
||||
},
|
||||
content: text,
|
||||
content: text_content,
|
||||
});
|
||||
TemplateElement::Container(ContainerElement {
|
||||
id: format!("{}_r{}c{}_wrap", table.id, row_idx, col_idx),
|
||||
position: PositionMode::Flow,
|
||||
size: SizeConstraint {
|
||||
width: effective_widths[col_idx].clone(),
|
||||
height: SizeValue::Auto,
|
||||
min_width: None,
|
||||
min_height: None,
|
||||
max_width: None,
|
||||
max_height: None,
|
||||
},
|
||||
direction: "column".to_string(),
|
||||
gap: 0.0,
|
||||
padding: Padding {
|
||||
top: cell_pad_v,
|
||||
right: cell_pad_h,
|
||||
bottom: cell_pad_v,
|
||||
left: cell_pad_h,
|
||||
},
|
||||
align: "stretch".to_string(),
|
||||
justify: "start".to_string(),
|
||||
style: ContainerStyle::default(),
|
||||
children: vec![text],
|
||||
break_inside: "auto".to_string(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
// row_idx 0-based: 0. satır görsel olarak 1. (tek/odd), 1. satır 2. (çift/even)
|
||||
let bg = if row_idx % 2 == 0 {
|
||||
// row_idx 0-based: çift index (0,2,4) renksiz, tek index (1,3,5) zebra rengi
|
||||
let bg = if row_idx % 2 == 1 {
|
||||
table.style.zebra_odd.clone()
|
||||
} else {
|
||||
table.style.zebra_even.clone()
|
||||
@@ -152,12 +349,12 @@ pub fn expand_table(
|
||||
direction: "row".to_string(),
|
||||
gap: 0.0,
|
||||
padding: Padding {
|
||||
top: 0.5,
|
||||
top: 0.0,
|
||||
right: 0.0,
|
||||
bottom: 0.5,
|
||||
bottom: 0.0,
|
||||
left: 0.0,
|
||||
},
|
||||
align: "center".to_string(),
|
||||
align: "stretch".to_string(),
|
||||
justify: "start".to_string(),
|
||||
style: ContainerStyle {
|
||||
background_color: bg,
|
||||
@@ -197,6 +394,8 @@ pub fn expand_table(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::data_resolve::{ResolvedData, ResolvedTable};
|
||||
use crate::text_measure::TextMeasurer;
|
||||
use crate::FontData;
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn make_table(num_columns: usize) -> RepeatingTableElement {
|
||||
@@ -239,6 +438,34 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn make_measurer() -> TextMeasurer {
|
||||
// Font dosyasını yükle
|
||||
let font_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("backend/fonts/NotoSans-Regular.ttf");
|
||||
let font_bytes = std::fs::read(&font_path).expect("Font file not found");
|
||||
let font_data = vec![FontData {
|
||||
family: "Noto Sans".to_string(),
|
||||
data: font_bytes,
|
||||
}];
|
||||
TextMeasurer::new(&font_data)
|
||||
}
|
||||
|
||||
/// Hücre wrapper container'ından içindeki StaticText'i çıkar
|
||||
fn unwrap_cell_text(cell: &TemplateElement) -> &StaticTextElement {
|
||||
match cell {
|
||||
TemplateElement::Container(c) => {
|
||||
assert_eq!(c.children.len(), 1, "Cell wrapper should have exactly 1 child");
|
||||
match &c.children[0] {
|
||||
TemplateElement::StaticText(t) => t,
|
||||
_ => panic!("Expected StaticText inside cell wrapper"),
|
||||
}
|
||||
}
|
||||
_ => panic!("Expected Container wrapper for cell"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expand_table_structure() {
|
||||
let table = make_table(2);
|
||||
@@ -246,8 +473,9 @@ mod tests {
|
||||
vec!["A".to_string(), "1".to_string()],
|
||||
vec!["B".to_string(), "2".to_string()],
|
||||
]);
|
||||
let mut measurer = make_measurer();
|
||||
|
||||
let container = expand_table(&table, &resolved);
|
||||
let container = expand_table(&table, &resolved, &mut measurer, 180.0);
|
||||
|
||||
// Wrapper container properties
|
||||
assert_eq!(container.id, "tbl");
|
||||
@@ -262,11 +490,9 @@ mod tests {
|
||||
assert_eq!(c.id, "tbl_header");
|
||||
assert_eq!(c.direction, "row");
|
||||
assert_eq!(c.children.len(), 2); // 2 columns
|
||||
// Check header cell text
|
||||
match &c.children[0] {
|
||||
TemplateElement::StaticText(t) => assert_eq!(t.content, "Column 0"),
|
||||
_ => panic!("Expected StaticText for header cell"),
|
||||
}
|
||||
// Check header cell text (inside wrapper container)
|
||||
let text = unwrap_cell_text(&c.children[0]);
|
||||
assert_eq!(text.content, "Column 0");
|
||||
}
|
||||
_ => panic!("Expected Container for header row"),
|
||||
}
|
||||
@@ -288,8 +514,9 @@ mod tests {
|
||||
fn test_expand_table_empty_data() {
|
||||
let table = make_table(3);
|
||||
let resolved = make_resolved("tbl", vec![]);
|
||||
let mut measurer = make_measurer();
|
||||
|
||||
let container = expand_table(&table, &resolved);
|
||||
let container = expand_table(&table, &resolved, &mut measurer, 180.0);
|
||||
|
||||
// Only header row, no data rows
|
||||
assert_eq!(container.children.len(), 1);
|
||||
@@ -309,8 +536,9 @@ mod tests {
|
||||
let resolved = make_resolved("tbl", vec![
|
||||
vec!["a".into(), "b".into(), "c".into(), "d".into()],
|
||||
]);
|
||||
let mut measurer = make_measurer();
|
||||
|
||||
let container = expand_table(&table, &resolved);
|
||||
let container = expand_table(&table, &resolved, &mut measurer, 180.0);
|
||||
|
||||
// header + 1 data row
|
||||
assert_eq!(container.children.len(), 2);
|
||||
@@ -332,20 +560,17 @@ mod tests {
|
||||
let resolved = make_resolved("tbl", vec![
|
||||
vec!["Hello".to_string(), "42".to_string()],
|
||||
]);
|
||||
let mut measurer = make_measurer();
|
||||
|
||||
let container = expand_table(&table, &resolved);
|
||||
let container = expand_table(&table, &resolved, &mut measurer, 180.0);
|
||||
|
||||
// Data row cells should contain the resolved text
|
||||
// Data row cells should contain the resolved text (inside wrapper containers)
|
||||
match &container.children[1] {
|
||||
TemplateElement::Container(c) => {
|
||||
match &c.children[0] {
|
||||
TemplateElement::StaticText(t) => assert_eq!(t.content, "Hello"),
|
||||
_ => panic!("Expected StaticText"),
|
||||
}
|
||||
match &c.children[1] {
|
||||
TemplateElement::StaticText(t) => assert_eq!(t.content, "42"),
|
||||
_ => panic!("Expected StaticText"),
|
||||
}
|
||||
let t0 = unwrap_cell_text(&c.children[0]);
|
||||
assert_eq!(t0.content, "Hello");
|
||||
let t1 = unwrap_cell_text(&c.children[1]);
|
||||
assert_eq!(t1.content, "42");
|
||||
}
|
||||
_ => panic!("Expected Container"),
|
||||
}
|
||||
@@ -358,8 +583,9 @@ mod tests {
|
||||
let resolved = make_resolved("tbl", vec![
|
||||
vec!["A".to_string(), "1".to_string()],
|
||||
]);
|
||||
let mut measurer = make_measurer();
|
||||
|
||||
let container = expand_table(&table, &resolved);
|
||||
let container = expand_table(&table, &resolved, &mut measurer, 180.0);
|
||||
|
||||
// header + separator line + 1 data row = 3
|
||||
assert_eq!(container.children.len(), 3);
|
||||
@@ -383,32 +609,103 @@ mod tests {
|
||||
vec!["row1".into()],
|
||||
vec!["row2".into()],
|
||||
]);
|
||||
let mut measurer = make_measurer();
|
||||
|
||||
let container = expand_table(&table, &resolved);
|
||||
let container = expand_table(&table, &resolved, &mut measurer, 180.0);
|
||||
|
||||
// header + 3 data rows
|
||||
assert_eq!(container.children.len(), 4);
|
||||
|
||||
// row_0 (even index) => zebra_odd
|
||||
// row_0 (even index) => zebra_even (no stripe)
|
||||
match &container.children[1] {
|
||||
TemplateElement::Container(c) => {
|
||||
assert_eq!(c.style.background_color, Some("#f0f0f0".to_string()));
|
||||
}
|
||||
_ => panic!("Expected Container"),
|
||||
}
|
||||
// row_1 (odd index) => zebra_even
|
||||
match &container.children[2] {
|
||||
TemplateElement::Container(c) => {
|
||||
assert_eq!(c.style.background_color, Some("#ffffff".to_string()));
|
||||
}
|
||||
_ => panic!("Expected Container"),
|
||||
}
|
||||
// row_2 (even index) => zebra_odd
|
||||
match &container.children[3] {
|
||||
// row_1 (odd index) => zebra_odd (striped)
|
||||
match &container.children[2] {
|
||||
TemplateElement::Container(c) => {
|
||||
assert_eq!(c.style.background_color, Some("#f0f0f0".to_string()));
|
||||
}
|
||||
_ => panic!("Expected Container"),
|
||||
}
|
||||
// row_2 (even index) => zebra_even (no stripe)
|
||||
match &container.children[3] {
|
||||
TemplateElement::Container(c) => {
|
||||
assert_eq!(c.style.background_color, Some("#ffffff".to_string()));
|
||||
}
|
||||
_ => panic!("Expected Container"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auto_columns_get_content_based_widths() {
|
||||
// Auto sütunlu tablo: genişlikler içeriğe göre hesaplanmalı
|
||||
let columns = vec![
|
||||
TableColumn {
|
||||
id: "col_0".into(),
|
||||
field: "short".into(),
|
||||
title: "No".into(),
|
||||
width: SizeValue::Auto,
|
||||
align: "right".into(),
|
||||
format: None,
|
||||
},
|
||||
TableColumn {
|
||||
id: "col_1".into(),
|
||||
field: "long".into(),
|
||||
title: "Urun / Hizmet Adi".into(),
|
||||
width: SizeValue::Auto,
|
||||
align: "left".into(),
|
||||
format: None,
|
||||
},
|
||||
];
|
||||
|
||||
let table = RepeatingTableElement {
|
||||
id: "tbl".to_string(),
|
||||
position: PositionMode::Flow,
|
||||
size: SizeConstraint {
|
||||
width: SizeValue::Fr { value: 1.0 },
|
||||
height: SizeValue::Auto,
|
||||
..Default::default()
|
||||
},
|
||||
data_source: ArrayBinding { path: "items".to_string() },
|
||||
columns,
|
||||
style: TableStyle::default(),
|
||||
repeat_header: Some(true),
|
||||
};
|
||||
|
||||
let resolved = make_resolved("tbl", vec![
|
||||
vec!["1".into(), "Web Uygulama Gelistirme".into()],
|
||||
vec!["2".into(), "SSL Sertifikasi".into()],
|
||||
]);
|
||||
let mut measurer = make_measurer();
|
||||
|
||||
let container = expand_table(&table, &resolved, &mut measurer, 180.0);
|
||||
|
||||
// Header row'daki ilk hücre wrapper (kısa: "No") ikinciden (uzun: "Urun / Hizmet Adi") dar olmalı
|
||||
match &container.children[0] {
|
||||
TemplateElement::Container(c) => {
|
||||
let w0 = match &c.children[0] {
|
||||
TemplateElement::Container(wrap) => match &wrap.size.width {
|
||||
SizeValue::Fixed { value } => *value,
|
||||
_ => panic!("Expected Fixed width for auto column wrapper"),
|
||||
},
|
||||
_ => panic!("Expected Container wrapper"),
|
||||
};
|
||||
let w1 = match &c.children[1] {
|
||||
TemplateElement::Container(wrap) => match &wrap.size.width {
|
||||
SizeValue::Fixed { value } => *value,
|
||||
_ => panic!("Expected Fixed width for auto column wrapper"),
|
||||
},
|
||||
_ => panic!("Expected Container wrapper"),
|
||||
};
|
||||
assert!(w1 > w0, "Long column ({w1}mm) should be wider than short column ({w0}mm)");
|
||||
// Her iki sütun toplamı available_width'e eşit olmalı
|
||||
let total = w0 + w1;
|
||||
assert!((total - 180.0).abs() < 0.1, "Total width ({total}mm) should equal available width (180mm)");
|
||||
}
|
||||
_ => panic!("Expected Container"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,17 +35,18 @@ pub fn compute(
|
||||
measurer: &mut TextMeasurer,
|
||||
) -> LayoutResult {
|
||||
let page_w_pt = mm_to_pt(template.page.width);
|
||||
let page_width_mm = template.page.width;
|
||||
|
||||
// --- 1. Header layout (varsa) ---
|
||||
let (header_elements, header_height_mm) = if let Some(ref header) = template.header {
|
||||
compute_section(header, page_w_pt, resolved, measurer)
|
||||
compute_section(header, page_w_pt, page_width_mm, resolved, measurer)
|
||||
} else {
|
||||
(vec![], 0.0)
|
||||
};
|
||||
|
||||
// --- 2. Footer layout (varsa) ---
|
||||
let (footer_elements, footer_height_mm) = if let Some(ref footer) = template.footer {
|
||||
compute_section(footer, page_w_pt, resolved, measurer)
|
||||
compute_section(footer, page_w_pt, page_width_mm, resolved, measurer)
|
||||
} else {
|
||||
(vec![], 0.0)
|
||||
};
|
||||
@@ -55,12 +56,15 @@ pub fn compute(
|
||||
taffy.disable_rounding();
|
||||
let mut node_map: HashMap<NodeId, NodeInfo> = HashMap::new();
|
||||
|
||||
let page_width_mm = template.page.width;
|
||||
let root_node = build_container(
|
||||
&template.root,
|
||||
&mut taffy,
|
||||
&mut node_map,
|
||||
resolved,
|
||||
None,
|
||||
measurer,
|
||||
page_width_mm,
|
||||
);
|
||||
|
||||
// Sayfa wrapper: sayfa genişliğinde ama yükseklik sınırsız (auto)
|
||||
@@ -117,6 +121,7 @@ pub fn compute(
|
||||
fn compute_section(
|
||||
container: &ContainerElement,
|
||||
page_w_pt: f32,
|
||||
page_width_mm: f64,
|
||||
resolved: &ResolvedData,
|
||||
measurer: &mut TextMeasurer,
|
||||
) -> (Vec<ElementLayout>, f64) {
|
||||
@@ -124,7 +129,7 @@ fn compute_section(
|
||||
taffy.disable_rounding();
|
||||
let mut node_map: HashMap<NodeId, NodeInfo> = HashMap::new();
|
||||
|
||||
let section_node = build_container(container, &mut taffy, &mut node_map, resolved, None);
|
||||
let section_node = build_container(container, &mut taffy, &mut node_map, resolved, None, measurer, page_width_mm);
|
||||
|
||||
let wrapper_style = Style {
|
||||
display: Display::Flex,
|
||||
@@ -182,15 +187,27 @@ fn build_container(
|
||||
node_map: &mut HashMap<NodeId, NodeInfo>,
|
||||
resolved: &ResolvedData,
|
||||
parent_direction: Option<&str>,
|
||||
measurer: &mut TextMeasurer,
|
||||
page_width_mm: f64,
|
||||
) -> NodeId {
|
||||
let style = sizing::container_to_style(el, parent_direction);
|
||||
let direction = el.direction.as_str();
|
||||
|
||||
// Child'lar için kullanılabilir genişliği hesapla
|
||||
// Container'ın kendi padding ve border'ını çıkar
|
||||
let border_w = el.style.border_width.unwrap_or(0.0);
|
||||
let container_own_width = match &el.size.width {
|
||||
SizeValue::Fixed { value } => *value,
|
||||
_ => page_width_mm, // Fr veya Auto ise parent'ın genişliğini kullan
|
||||
};
|
||||
let content_width_mm = container_own_width - el.padding.left - el.padding.right - border_w * 2.0;
|
||||
let content_width_mm = content_width_mm.max(0.0);
|
||||
|
||||
let mut child_nodes = Vec::new();
|
||||
let mut children_ids = Vec::new();
|
||||
|
||||
for child in &el.children {
|
||||
let child_node = build_element(child, taffy, node_map, resolved, Some(direction));
|
||||
let child_node = build_element(child, taffy, node_map, resolved, Some(direction), measurer, content_width_mm);
|
||||
child_nodes.push(child_node);
|
||||
children_ids.push(child.id().to_string());
|
||||
}
|
||||
@@ -225,10 +242,12 @@ fn build_element(
|
||||
node_map: &mut HashMap<NodeId, NodeInfo>,
|
||||
resolved: &ResolvedData,
|
||||
parent_direction: Option<&str>,
|
||||
measurer: &mut TextMeasurer,
|
||||
page_width_mm: f64,
|
||||
) -> NodeId {
|
||||
match el {
|
||||
TemplateElement::Container(e) => {
|
||||
build_container(e, taffy, node_map, resolved, parent_direction)
|
||||
build_container(e, taffy, node_map, resolved, parent_direction, measurer, page_width_mm)
|
||||
}
|
||||
TemplateElement::StaticText(e) => build_text_leaf(
|
||||
taffy,
|
||||
@@ -396,8 +415,8 @@ fn build_element(
|
||||
node
|
||||
}
|
||||
TemplateElement::RepeatingTable(e) => {
|
||||
// Tabloyu container ağacına expand et
|
||||
let expanded = table_layout::expand_table(e, resolved);
|
||||
// Tabloyu container ağacına expand et (measurer ile auto sütun genişlikleri hesaplanır)
|
||||
let expanded = table_layout::expand_table(e, resolved, measurer, page_width_mm);
|
||||
|
||||
// Expand edilmiş tablo cell'lerinin text'lerini resolved'a ekle
|
||||
// (expand_table StaticText'ler üretir, bunların text'leri zaten content'te)
|
||||
@@ -414,6 +433,8 @@ fn build_element(
|
||||
node_map,
|
||||
&table_resolved,
|
||||
parent_direction,
|
||||
measurer,
|
||||
page_width_mm,
|
||||
)
|
||||
}
|
||||
TemplateElement::Shape(e) => {
|
||||
|
||||
Reference in New Issue
Block a user