repofactor

This commit is contained in:
2026-04-09 00:15:05 +03:00
parent e574889e5d
commit 4fda0e7d98
13 changed files with 700 additions and 883 deletions

View File

@@ -247,11 +247,8 @@ pub struct ChartStyle {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChartElement {
pub id: String,
#[serde(default)]
pub condition: Option<Condition>,
pub position: PositionMode,
pub size: SizeConstraint,
#[serde(flatten)]
pub base: ElementBase,
pub chart_type: ChartType,
pub data_source: ArrayBinding,
pub category_field: String,
@@ -272,6 +269,138 @@ pub struct ChartElement {
pub style: ChartStyle,
}
// --- Element Base (ortak alanlar) ---
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ElementBase {
pub id: String,
#[serde(default)]
pub condition: Option<Condition>,
#[serde(default)]
pub position: PositionMode,
#[serde(default)]
pub size: SizeConstraint,
}
impl ElementBase {
/// Flow pozisyonlu, condition'sız, verilen size ile base oluştur
pub fn flow(id: String, size: SizeConstraint) -> Self {
Self {
id,
condition: None,
position: PositionMode::Flow,
size,
}
}
}
pub trait HasBase {
fn base(&self) -> &ElementBase;
fn base_mut(&mut self) -> &mut ElementBase;
}
macro_rules! impl_has_base {
($($t:ty),+ $(,)?) => {
$(impl HasBase for $t {
fn base(&self) -> &ElementBase { &self.base }
fn base_mut(&mut self) -> &mut ElementBase { &mut self.base }
})+
};
}
impl_has_base!(
ContainerElement,
StaticTextElement,
TextElement,
LineElement,
ImageElement,
PageNumberElement,
BarcodeElement,
RepeatingTableElement,
PageBreakElement,
CurrentDateElement,
ShapeElement,
CheckboxElement,
CalculatedTextElement,
RichTextElement,
ChartElement,
);
pub trait ElementTypeStr {
fn type_str(&self) -> &'static str;
}
macro_rules! impl_type_str {
($($t:ty => $s:literal),+ $(,)?) => {
$(impl ElementTypeStr for $t {
fn type_str(&self) -> &'static str { $s }
})+
};
}
impl_type_str!(
ContainerElement => "container",
StaticTextElement => "static_text",
TextElement => "text",
LineElement => "line",
ImageElement => "image",
PageNumberElement => "page_number",
BarcodeElement => "barcode",
RepeatingTableElement => "repeating_table",
PageBreakElement => "page_break",
CurrentDateElement => "current_date",
ShapeElement => "shape",
CheckboxElement => "checkbox",
CalculatedTextElement => "calculated_text",
RichTextElement => "rich_text",
ChartElement => "chart",
);
pub trait HasTextStyle {
fn text_style(&self) -> &TextStyle;
}
macro_rules! impl_has_text_style {
($($t:ty),+ $(,)?) => {
$(impl HasTextStyle for $t {
fn text_style(&self) -> &TextStyle { &self.style }
})+
};
}
impl_has_text_style!(
StaticTextElement,
TextElement,
PageNumberElement,
CurrentDateElement,
CalculatedTextElement,
RichTextElement,
);
pub trait HasOptionalBinding {
fn binding(&self) -> Option<&ScalarBinding>;
fn static_value(&self) -> Option<&str>;
}
impl HasOptionalBinding for ImageElement {
fn binding(&self) -> Option<&ScalarBinding> {
self.binding.as_ref()
}
fn static_value(&self) -> Option<&str> {
self.src.as_deref()
}
}
impl HasOptionalBinding for BarcodeElement {
fn binding(&self) -> Option<&ScalarBinding> {
self.binding.as_ref()
}
fn static_value(&self) -> Option<&str> {
self.value.as_deref()
}
}
// --- Element tipleri ---
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
@@ -316,91 +445,59 @@ pub enum TemplateElement {
}
impl TemplateElement {
pub fn id(&self) -> &str {
fn inner_base(&self) -> &ElementBase {
match self {
Self::Container(e) => &e.id,
Self::StaticText(e) => &e.id,
Self::Text(e) => &e.id,
Self::Line(e) => &e.id,
Self::RepeatingTable(e) => &e.id,
Self::Image(e) => &e.id,
Self::PageNumber(e) => &e.id,
Self::Barcode(e) => &e.id,
Self::PageBreak(e) => &e.id,
Self::CurrentDate(e) => &e.id,
Self::Shape(e) => &e.id,
Self::Checkbox(e) => &e.id,
Self::CalculatedText(e) => &e.id,
Self::RichText(e) => &e.id,
Self::Chart(e) => &e.id,
Self::Container(e) => e.base(),
Self::StaticText(e) => e.base(),
Self::Text(e) => e.base(),
Self::Line(e) => e.base(),
Self::RepeatingTable(e) => e.base(),
Self::Image(e) => e.base(),
Self::PageNumber(e) => e.base(),
Self::Barcode(e) => e.base(),
Self::PageBreak(e) => e.base(),
Self::CurrentDate(e) => e.base(),
Self::Shape(e) => e.base(),
Self::Checkbox(e) => e.base(),
Self::CalculatedText(e) => e.base(),
Self::RichText(e) => e.base(),
Self::Chart(e) => e.base(),
}
}
pub fn id(&self) -> &str {
&self.inner_base().id
}
pub fn position(&self) -> &PositionMode {
match self {
Self::Container(e) => &e.position,
Self::StaticText(e) => &e.position,
Self::Text(e) => &e.position,
Self::Line(e) => &e.position,
Self::RepeatingTable(e) => &e.position,
Self::Image(e) => &e.position,
Self::PageNumber(e) => &e.position,
Self::Barcode(e) => &e.position,
Self::PageBreak(_) => &PositionMode::Flow,
Self::CurrentDate(e) => &e.position,
Self::Shape(e) => &e.position,
Self::Checkbox(e) => &e.position,
Self::CalculatedText(e) => &e.position,
Self::RichText(e) => &e.position,
Self::Chart(e) => &e.position,
}
&self.inner_base().position
}
pub fn condition(&self) -> Option<&Condition> {
match self {
Self::Container(e) => e.condition.as_ref(),
Self::StaticText(e) => e.condition.as_ref(),
Self::Text(e) => e.condition.as_ref(),
Self::Line(e) => e.condition.as_ref(),
Self::RepeatingTable(e) => e.condition.as_ref(),
Self::Image(e) => e.condition.as_ref(),
Self::PageNumber(e) => e.condition.as_ref(),
Self::Barcode(e) => e.condition.as_ref(),
Self::PageBreak(e) => e.condition.as_ref(),
Self::CurrentDate(e) => e.condition.as_ref(),
Self::Shape(e) => e.condition.as_ref(),
Self::Checkbox(e) => e.condition.as_ref(),
Self::CalculatedText(e) => e.condition.as_ref(),
Self::RichText(e) => e.condition.as_ref(),
Self::Chart(e) => e.condition.as_ref(),
}
self.inner_base().condition.as_ref()
}
pub fn size(&self) -> &SizeConstraint {
static DEFAULT_SIZE: SizeConstraint = SizeConstraint {
width: SizeValue::Auto,
height: SizeValue::Auto,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
};
&self.inner_base().size
}
pub fn type_str(&self) -> &'static str {
match self {
Self::Container(e) => &e.size,
Self::StaticText(e) => &e.size,
Self::Text(e) => &e.size,
Self::Line(e) => &e.size,
Self::RepeatingTable(e) => &e.size,
Self::Image(e) => &e.size,
Self::PageNumber(e) => &e.size,
Self::Barcode(e) => &e.size,
Self::PageBreak(_) => &DEFAULT_SIZE,
Self::CurrentDate(e) => &e.size,
Self::Shape(e) => &e.size,
Self::Checkbox(e) => &e.size,
Self::CalculatedText(e) => &e.size,
Self::RichText(e) => &e.size,
Self::Chart(e) => &e.size,
Self::Container(e) => e.type_str(),
Self::StaticText(e) => e.type_str(),
Self::Text(e) => e.type_str(),
Self::Line(e) => e.type_str(),
Self::RepeatingTable(e) => e.type_str(),
Self::Image(e) => e.type_str(),
Self::PageNumber(e) => e.type_str(),
Self::Barcode(e) => e.type_str(),
Self::PageBreak(e) => e.type_str(),
Self::CurrentDate(e) => e.type_str(),
Self::Shape(e) => e.type_str(),
Self::Checkbox(e) => e.type_str(),
Self::CalculatedText(e) => e.type_str(),
Self::RichText(e) => e.type_str(),
Self::Chart(e) => e.type_str(),
}
}
}
@@ -408,11 +505,8 @@ impl TemplateElement {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RichTextElement {
pub id: String,
#[serde(default)]
pub condition: Option<Condition>,
pub position: PositionMode,
pub size: SizeConstraint,
#[serde(flatten)]
pub base: ElementBase,
#[serde(default)]
pub style: TextStyle, // varsayilan stil (span'lar override edebilir)
pub content: Vec<RichTextSpan>,
@@ -421,13 +515,8 @@ pub struct RichTextElement {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContainerElement {
pub id: String,
#[serde(default)]
pub condition: Option<Condition>,
#[serde(default)]
pub position: PositionMode,
#[serde(default)]
pub size: SizeConstraint,
#[serde(flatten)]
pub base: ElementBase,
#[serde(default = "default_column")]
pub direction: String,
#[serde(default)]
@@ -463,11 +552,8 @@ fn default_start() -> String {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StaticTextElement {
pub id: String,
#[serde(default)]
pub condition: Option<Condition>,
pub position: PositionMode,
pub size: SizeConstraint,
#[serde(flatten)]
pub base: ElementBase,
pub style: TextStyle,
pub content: String,
}
@@ -475,11 +561,8 @@ pub struct StaticTextElement {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TextElement {
pub id: String,
#[serde(default)]
pub condition: Option<Condition>,
pub position: PositionMode,
pub size: SizeConstraint,
#[serde(flatten)]
pub base: ElementBase,
pub style: TextStyle,
pub content: Option<String>,
pub binding: ScalarBinding,
@@ -488,22 +571,16 @@ pub struct TextElement {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LineElement {
pub id: String,
#[serde(default)]
pub condition: Option<Condition>,
pub position: PositionMode,
pub size: SizeConstraint,
#[serde(flatten)]
pub base: ElementBase,
pub style: LineStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ImageElement {
pub id: String,
#[serde(default)]
pub condition: Option<Condition>,
pub position: PositionMode,
pub size: SizeConstraint,
#[serde(flatten)]
pub base: ElementBase,
pub src: Option<String>,
pub binding: Option<ScalarBinding>,
pub style: ImageStyle,
@@ -512,11 +589,8 @@ pub struct ImageElement {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PageNumberElement {
pub id: String,
#[serde(default)]
pub condition: Option<Condition>,
pub position: PositionMode,
pub size: SizeConstraint,
#[serde(flatten)]
pub base: ElementBase,
pub style: TextStyle,
pub format: Option<String>,
}
@@ -524,11 +598,8 @@ pub struct PageNumberElement {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BarcodeElement {
pub id: String,
#[serde(default)]
pub condition: Option<Condition>,
pub position: PositionMode,
pub size: SizeConstraint,
#[serde(flatten)]
pub base: ElementBase,
pub format: String, // qr, ean13, ean8, code128, code39
pub value: Option<String>,
pub binding: Option<ScalarBinding>,
@@ -538,11 +609,8 @@ pub struct BarcodeElement {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RepeatingTableElement {
pub id: String,
#[serde(default)]
pub condition: Option<Condition>,
pub position: PositionMode,
pub size: SizeConstraint,
#[serde(flatten)]
pub base: ElementBase,
pub data_source: ArrayBinding,
pub columns: Vec<TableColumn>,
pub style: TableStyle,
@@ -557,19 +625,15 @@ fn default_true() -> Option<bool> {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PageBreakElement {
pub id: String,
#[serde(default)]
pub condition: Option<Condition>,
#[serde(flatten)]
pub base: ElementBase,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CurrentDateElement {
pub id: String,
#[serde(default)]
pub condition: Option<Condition>,
pub position: PositionMode,
pub size: SizeConstraint,
#[serde(flatten)]
pub base: ElementBase,
pub style: TextStyle,
pub format: Option<String>,
}
@@ -577,11 +641,8 @@ pub struct CurrentDateElement {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ShapeElement {
pub id: String,
#[serde(default)]
pub condition: Option<Condition>,
pub position: PositionMode,
pub size: SizeConstraint,
#[serde(flatten)]
pub base: ElementBase,
pub shape_type: String, // rectangle, ellipse, rounded_rectangle
pub style: ContainerStyle,
}
@@ -598,11 +659,8 @@ pub struct CheckboxStyle {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CheckboxElement {
pub id: String,
#[serde(default)]
pub condition: Option<Condition>,
pub position: PositionMode,
pub size: SizeConstraint,
#[serde(flatten)]
pub base: ElementBase,
pub checked: Option<bool>, // statik değer
pub binding: Option<ScalarBinding>, // dinamik boolean binding
pub style: CheckboxStyle,
@@ -611,11 +669,8 @@ pub struct CheckboxElement {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CalculatedTextElement {
pub id: String,
#[serde(default)]
pub condition: Option<Condition>,
pub position: PositionMode,
pub size: SizeConstraint,
#[serde(flatten)]
pub base: ElementBase,
pub style: TextStyle,
pub expression: String,
pub format: Option<String>,

View File

@@ -116,10 +116,19 @@ export interface BarcodeStyle {
includeText?: boolean // barkod altına değer yazılsın mı (QR hariç)
}
// --- Condition (koşullu gösterim) ---
export interface Condition {
path: string
operator: string
value?: unknown
}
// --- Element tipleri ---
interface BaseElement {
id: string
condition?: Condition
position: PositionMode
size: SizeConstraint
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 166 KiB

View File

@@ -2,6 +2,9 @@ use dreport_core::models::*;
use serde_json::Value;
use std::collections::HashMap;
// Re-export HasOptionalBinding for convenience
pub use dreport_core::models::HasOptionalBinding;
/// Şu anki tarihi verilen format string'ine göre formatla.
/// Desteklenen tokenlar: YYYY, MM, DD, HH, mm, ss
/// WASM'da js_sys::Date, native'de SystemTime kullanır.
@@ -226,6 +229,15 @@ fn json_values_eq(a: &Value, b: &Value) -> bool {
}
}
/// Çözümle optional binding: binding varsa data'dan, yoksa static value'dan
fn resolve_optional_binding(el: &impl HasOptionalBinding, data: &Value) -> String {
if let Some(binding) = el.binding() {
value_to_string(resolve_path(data, &binding.path))
} else {
el.static_value().unwrap_or_default().to_string()
}
}
fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedData, format_config: &dreport_core::models::FormatConfig) {
// Koşul kontrolü: condition varsa ve sağlanmıyorsa, hidden olarak işaretle ve çık
if let Some(condition) = el.condition() && !evaluate_condition(condition, data) {
@@ -235,7 +247,7 @@ fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedDa
match el {
TemplateElement::StaticText(e) => {
resolved.texts.insert(e.id.clone(), e.content.clone());
resolved.texts.insert(e.base.id.clone(), e.content.clone());
}
TemplateElement::Text(e) => {
let bound_value = value_to_string(resolve_path(data, &e.binding.path));
@@ -243,7 +255,7 @@ fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedDa
Some(prefix) if !prefix.is_empty() => format!("{}{}", prefix, bound_value),
_ => bound_value,
};
resolved.texts.insert(e.id.clone(), text);
resolved.texts.insert(e.base.id.clone(), text);
}
TemplateElement::PageNumber(e) => {
// Format string'i sakla — sayfa bölme sonrası gerçek değerlerle çözülecek
@@ -254,28 +266,18 @@ fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedDa
.to_string();
resolved
.page_number_formats
.insert(e.id.clone(), fmt.clone());
.insert(e.base.id.clone(), fmt.clone());
// Placeholder koy (tek sayfalık fallback)
resolved.texts.insert(
e.id.clone(),
e.base.id.clone(),
fmt.replace("{current}", "1").replace("{total}", "1"),
);
}
TemplateElement::Barcode(e) => {
let value = if let Some(binding) = &e.binding {
value_to_string(resolve_path(data, &binding.path))
} else {
e.value.clone().unwrap_or_default()
};
resolved.barcodes.insert(e.id.clone(), value);
resolved.barcodes.insert(e.base.id.clone(), resolve_optional_binding(e, data));
}
TemplateElement::Image(e) => {
let src = if let Some(binding) = &e.binding {
value_to_string(resolve_path(data, &binding.path))
} else {
e.src.clone().unwrap_or_default()
};
resolved.images.insert(e.id.clone(), src);
resolved.images.insert(e.base.id.clone(), resolve_optional_binding(e, data));
}
TemplateElement::RepeatingTable(e) => {
let array = resolve_path(data, &e.data_source.path);
@@ -302,7 +304,7 @@ fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedDa
}
_ => vec![],
};
resolved.tables.insert(e.id.clone(), ResolvedTable { rows });
resolved.tables.insert(e.base.id.clone(), ResolvedTable { rows });
}
TemplateElement::Container(e) => {
for child in &e.children {
@@ -312,7 +314,7 @@ fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedDa
TemplateElement::CurrentDate(e) => {
let fmt = e.format.as_deref().unwrap_or("DD.MM.YYYY");
let text = format_current_date(fmt);
resolved.texts.insert(e.id.clone(), text);
resolved.texts.insert(e.base.id.clone(), text);
}
TemplateElement::Checkbox(e) => {
let checked = if let Some(binding) = &e.binding {
@@ -327,7 +329,7 @@ fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedDa
e.checked.unwrap_or(false)
};
// Store as "true"/"false" string in texts map
resolved.texts.insert(e.id.clone(), checked.to_string());
resolved.texts.insert(e.base.id.clone(), checked.to_string());
}
TemplateElement::CalculatedText(e) => {
let result = crate::expr_eval::evaluate_expression(&e.expression, data);
@@ -338,7 +340,7 @@ fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedDa
} else {
formatted
};
resolved.texts.insert(e.id.clone(), text);
resolved.texts.insert(e.base.id.clone(), text);
}
TemplateElement::RichText(e) => {
let spans: Vec<ResolvedRichSpan> = e
@@ -371,7 +373,7 @@ fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedDa
}
})
.collect();
resolved.rich_texts.insert(e.id.clone(), spans);
resolved.rich_texts.insert(e.base.id.clone(), spans);
}
TemplateElement::Chart(e) => {
let array = resolve_path(data, &e.data_source.path);
@@ -389,7 +391,7 @@ fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedDa
group_mode: e.group_mode.clone(),
},
};
resolved.charts.insert(e.id.clone(), chart_data);
resolved.charts.insert(e.base.id.clone(), chart_data);
}
TemplateElement::Line(_) => {}
TemplateElement::Shape(_) => {}
@@ -542,10 +544,7 @@ mod tests {
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("root".to_string(), SizeConstraint::default()),
direction: "column".to_string(),
gap: 0.0,
padding: Padding::default(),
@@ -554,10 +553,7 @@ mod tests {
style: ContainerStyle::default(),
break_inside: "auto".to_string(),
children: vec![TemplateElement::Text(TextElement {
id: "el_name".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("el_name".to_string(), SizeConstraint::default()),
style: TextStyle::default(),
content: None,
binding: ScalarBinding {
@@ -593,10 +589,7 @@ mod tests {
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("root".to_string(), SizeConstraint::default()),
direction: "column".to_string(),
gap: 0.0,
padding: Padding::default(),
@@ -605,10 +598,7 @@ mod tests {
style: ContainerStyle::default(),
break_inside: "auto".to_string(),
children: vec![TemplateElement::Text(TextElement {
id: "el_no".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("el_no".to_string(), SizeConstraint::default()),
style: TextStyle::default(),
content: Some("Fatura No: ".to_string()),
binding: ScalarBinding {
@@ -641,10 +631,7 @@ mod tests {
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("root".to_string(), SizeConstraint::default()),
direction: "column".to_string(),
gap: 0.0,
padding: Padding::default(),
@@ -653,10 +640,7 @@ mod tests {
style: ContainerStyle::default(),
break_inside: "auto".to_string(),
children: vec![TemplateElement::StaticText(StaticTextElement {
id: "title".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("title".to_string(), SizeConstraint::default()),
style: TextStyle::default(),
content: "FATURA".to_string(),
})],
@@ -682,10 +666,7 @@ mod tests {
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("root".to_string(), SizeConstraint::default()),
direction: "column".to_string(),
gap: 0.0,
padding: Padding::default(),
@@ -694,10 +675,7 @@ mod tests {
style: ContainerStyle::default(),
break_inside: "auto".to_string(),
children: vec![TemplateElement::RepeatingTable(RepeatingTableElement {
id: "tbl".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("tbl".to_string(), SizeConstraint::default()),
data_source: ArrayBinding {
path: "kalemler".to_string(),
},
@@ -754,10 +732,7 @@ mod tests {
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("root".to_string(), SizeConstraint::default()),
direction: "column".to_string(),
gap: 0.0,
padding: Padding::default(),
@@ -766,10 +741,7 @@ mod tests {
style: ContainerStyle::default(),
break_inside: "auto".to_string(),
children: vec![TemplateElement::RepeatingTable(RepeatingTableElement {
id: "tbl".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("tbl".to_string(), SizeConstraint::default()),
data_source: ArrayBinding {
path: "items".to_string(),
},
@@ -808,10 +780,7 @@ mod tests {
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("root".to_string(), SizeConstraint::default()),
direction: "column".to_string(),
gap: 0.0,
padding: Padding::default(),
@@ -820,10 +789,7 @@ mod tests {
style: ContainerStyle::default(),
break_inside: "auto".to_string(),
children: vec![TemplateElement::Text(TextElement {
id: "el_missing".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("el_missing".to_string(), SizeConstraint::default()),
style: TextStyle::default(),
content: None,
binding: ScalarBinding {

View File

@@ -223,6 +223,75 @@ pub struct ResolvedStyle {
pub barcode_include_text: Option<bool>,
}
// --- From<&XStyle> for ResolvedStyle ---
impl From<&dreport_core::models::TextStyle> for ResolvedStyle {
fn from(s: &dreport_core::models::TextStyle) -> Self {
Self {
font_size: s.font_size,
font_weight: s.font_weight.clone(),
font_style: s.font_style.clone(),
font_family: s.font_family.clone(),
color: s.color.clone(),
text_align: s.align.clone(),
..Default::default()
}
}
}
impl From<&dreport_core::models::ContainerStyle> for ResolvedStyle {
fn from(s: &dreport_core::models::ContainerStyle) -> Self {
Self {
background_color: s.background_color.clone(),
border_color: s.border_color.clone(),
border_width: s.border_width,
border_radius: s.border_radius,
border_style: s.border_style.clone(),
..Default::default()
}
}
}
impl From<&dreport_core::models::LineStyle> for ResolvedStyle {
fn from(s: &dreport_core::models::LineStyle) -> Self {
Self {
stroke_color: s.stroke_color.clone(),
stroke_width: s.stroke_width,
..Default::default()
}
}
}
impl From<&dreport_core::models::ImageStyle> for ResolvedStyle {
fn from(s: &dreport_core::models::ImageStyle) -> Self {
Self {
object_fit: s.object_fit.clone(),
..Default::default()
}
}
}
impl From<&dreport_core::models::BarcodeStyle> for ResolvedStyle {
fn from(s: &dreport_core::models::BarcodeStyle) -> Self {
Self {
barcode_color: s.color.clone(),
barcode_include_text: s.include_text,
..Default::default()
}
}
}
impl From<&dreport_core::models::CheckboxStyle> for ResolvedStyle {
fn from(s: &dreport_core::models::CheckboxStyle) -> Self {
Self {
color: s.check_color.clone(),
border_color: s.border_color.clone(),
border_width: s.border_width,
..Default::default()
}
}
}
/// Ana layout hesaplama fonksiyonu.
/// Template + data + font verileri alır, her element için pozisyon döner.
pub fn compute_layout(

View File

@@ -1603,17 +1603,14 @@ mod tests {
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
base: ElementBase::flow("root".to_string(), SizeConstraint {
width: SizeValue::Auto,
height: SizeValue::Auto,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
},
}),
direction: "column".to_string(),
gap: 5.0,
padding: Padding {
@@ -1628,17 +1625,14 @@ mod tests {
break_inside: "auto".to_string(),
children: vec![
TemplateElement::StaticText(StaticTextElement {
id: "title".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
base: ElementBase::flow("title".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
},
}),
style: TextStyle {
font_size: Some(18.0),
font_weight: Some("bold".to_string()),
@@ -1647,34 +1641,28 @@ mod tests {
content: "FATURA".to_string(),
}),
TemplateElement::Line(LineElement {
id: "line1".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
base: ElementBase::flow("line1".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
},
}),
style: LineStyle {
stroke_color: Some("#000000".to_string()),
stroke_width: Some(0.5),
},
}),
TemplateElement::Text(TextElement {
id: "firma".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
base: ElementBase::flow("firma".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
},
}),
style: TextStyle {
font_size: Some(11.0),
..Default::default()

View File

@@ -138,7 +138,7 @@ pub fn container_to_style(el: &ContainerElement, parent_direction: Option<&str>)
};
// Pozisyon moduna göre
match &el.position {
match &el.base.position {
PositionMode::Absolute { x, y } => {
style.position = Position::Absolute;
style.inset = Rect {
@@ -152,7 +152,7 @@ pub fn container_to_style(el: &ContainerElement, parent_direction: Option<&str>)
}
// Boyut
apply_size_to_style(&mut style, &el.size, parent_direction);
apply_size_to_style(&mut style, &el.base.size, parent_direction);
// Container border
if let Some(bw) = el.style.border_width {
@@ -197,7 +197,7 @@ pub fn leaf_style(
#[cfg(test)]
mod tests {
use super::*;
use dreport_core::models::{ContainerStyle, Padding};
use dreport_core::models::{ContainerStyle, ElementBase, Padding};
#[test]
fn test_mm_to_pt_conversion() {
@@ -327,10 +327,7 @@ mod tests {
#[test]
fn test_container_to_style_direction() {
let el = ContainerElement {
id: "test".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("test".to_string(), SizeConstraint::default()),
direction: "row".to_string(),
gap: 5.0,
padding: Padding {
@@ -354,10 +351,12 @@ mod tests {
#[test]
fn test_container_to_style_absolute() {
let el = ContainerElement {
id: "test".to_string(),
condition: None,
position: PositionMode::Absolute { x: 20.0, y: 30.0 },
size: SizeConstraint::default(),
base: ElementBase {
id: "test".to_string(),
condition: None,
position: PositionMode::Absolute { x: 20.0, y: 30.0 },
size: SizeConstraint::default(),
},
direction: "column".to_string(),
gap: 0.0,
padding: Padding::default(),

View File

@@ -186,7 +186,7 @@ pub fn expand_table_cached(
) -> ContainerElement {
let rows = resolved
.tables
.get(&table.id)
.get(&table.base.id)
.map(|t| t.rows.as_slice())
.unwrap_or(&[]);
let key = table_cache_key(table, rows, available_width_mm);
@@ -211,7 +211,7 @@ pub fn expand_table(
measurer: &mut TextMeasurer,
available_width_mm: f64,
) -> ContainerElement {
let resolved_table = resolved.tables.get(&table.id);
let resolved_table = resolved.tables.get(&table.base.id);
let rows = resolved_table.map(|t| t.rows.as_slice()).unwrap_or(&[]);
// Auto sütunlar için içerik bazlı genişlik hesapla
@@ -232,17 +232,14 @@ pub fn expand_table(
.enumerate()
.map(|(i, col)| {
let text = TemplateElement::StaticText(StaticTextElement {
id: format!("{}_hdr_{}", table.id, i),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
},
base: ElementBase::flow(
format!("{}_hdr_{}", table.base.id, i),
SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
),
style: TextStyle {
font_size: table.style.header_font_size.or(table.style.font_size),
font_weight: Some("bold".to_string()),
@@ -254,25 +251,13 @@ pub fn expand_table(
content: col.title.clone(),
});
TemplateElement::Container(ContainerElement {
id: format!("{}_hdr_{}_wrap", table.id, i),
condition: None,
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,
},
base: ElementBase::flow(
format!("{}_hdr_{}_wrap", table.base.id, i),
SizeConstraint { width: effective_widths[i].clone(), ..Default::default() },
),
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,
},
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(),
@@ -283,31 +268,16 @@ pub fn expand_table(
.collect();
children.push(TemplateElement::Container(ContainerElement {
id: format!("{}_header", table.id),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
},
base: ElementBase::flow(
format!("{}_header", table.base.id),
SizeConstraint { width: SizeValue::Fr { value: 1.0 }, ..Default::default() },
),
direction: "row".to_string(),
gap: 0.0,
padding: Padding {
top: 0.0,
right: 0.0,
bottom: 0.0,
left: 0.0,
},
padding: Padding::default(),
align: "stretch".to_string(),
justify: "start".to_string(),
style: ContainerStyle {
background_color: table.style.header_bg.clone(),
..Default::default()
},
style: ContainerStyle { background_color: table.style.header_bg.clone(), ..Default::default() },
children: header_cells,
break_inside: "auto".to_string(),
}));
@@ -315,17 +285,10 @@ pub fn expand_table(
// Header altına ayırıcı çizgi
if table.style.border_color.is_some() {
children.push(TemplateElement::Line(LineElement {
id: format!("{}_header_line", table.id),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
},
base: ElementBase::flow(
format!("{}_header_line", table.base.id),
SizeConstraint { width: SizeValue::Fr { value: 1.0 }, ..Default::default() },
),
style: LineStyle {
stroke_color: table.style.border_color.clone(),
stroke_width: table.style.border_width,
@@ -343,17 +306,10 @@ pub fn expand_table(
let text_content = row_data.get(col_idx).cloned().unwrap_or_default();
let text = TemplateElement::StaticText(StaticTextElement {
id: format!("{}_r{}c{}", table.id, row_idx, col_idx),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
},
base: ElementBase::flow(
format!("{}_r{}c{}", table.base.id, row_idx, col_idx),
SizeConstraint { width: SizeValue::Fr { value: 1.0 }, ..Default::default() },
),
style: TextStyle {
font_size: table.style.font_size,
font_weight: None,
@@ -365,25 +321,13 @@ pub fn expand_table(
content: text_content,
});
TemplateElement::Container(ContainerElement {
id: format!("{}_r{}c{}_wrap", table.id, row_idx, col_idx),
condition: None,
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,
},
base: ElementBase::flow(
format!("{}_r{}c{}_wrap", table.base.id, row_idx, col_idx),
SizeConstraint { width: effective_widths[col_idx].clone(), ..Default::default() },
),
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,
},
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(),
@@ -401,31 +345,16 @@ pub fn expand_table(
};
children.push(TemplateElement::Container(ContainerElement {
id: format!("{}_row_{}", table.id, row_idx),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
},
base: ElementBase::flow(
format!("{}_row_{}", table.base.id, row_idx),
SizeConstraint { width: SizeValue::Fr { value: 1.0 }, ..Default::default() },
),
direction: "row".to_string(),
gap: 0.0,
padding: Padding {
top: 0.0,
right: 0.0,
bottom: 0.0,
left: 0.0,
},
padding: Padding::default(),
align: "stretch".to_string(),
justify: "start".to_string(),
style: ContainerStyle {
background_color: bg,
..Default::default()
},
style: ContainerStyle { background_color: bg, ..Default::default() },
children: cells,
break_inside: "auto".to_string(),
}));
@@ -433,18 +362,15 @@ pub fn expand_table(
// Wrapper container (column direction, tüm tablo)
ContainerElement {
id: table.id.clone(),
condition: None,
position: table.position.clone(),
size: table.size.clone(),
base: ElementBase {
id: table.base.id.clone(),
condition: None,
position: table.base.position.clone(),
size: table.base.size.clone(),
},
direction: "column".to_string(),
gap: 0.0,
padding: Padding {
top: 0.0,
right: 0.0,
bottom: 0.0,
left: 0.0,
},
padding: Padding::default(),
align: "stretch".to_string(),
justify: "start".to_string(),
style: ContainerStyle {
@@ -478,14 +404,10 @@ mod tests {
.collect();
RepeatingTableElement {
id: "tbl".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
base: ElementBase::flow(
"tbl".to_string(),
SizeConstraint { width: SizeValue::Fr { value: 1.0 }, ..Default::default() },
),
data_source: ArrayBinding {
path: "items".to_string(),
},
@@ -554,7 +476,7 @@ mod tests {
let container = expand_table(&table, &resolved, &mut measurer, 180.0);
// Wrapper container properties
assert_eq!(container.id, "tbl");
assert_eq!(container.base.id, "tbl");
assert_eq!(container.direction, "column");
// Children: header row + 2 data rows (no border_color so no separator line)
@@ -563,7 +485,7 @@ mod tests {
// First child is header row container
match &container.children[0] {
TemplateElement::Container(c) => {
assert_eq!(c.id, "tbl_header");
assert_eq!(c.base.id, "tbl_header");
assert_eq!(c.direction, "row");
assert_eq!(c.children.len(), 2); // 2 columns
// Check header cell text (inside wrapper container)
@@ -577,7 +499,7 @@ mod tests {
for (row_idx, child) in container.children[1..].iter().enumerate() {
match child {
TemplateElement::Container(c) => {
assert_eq!(c.id, format!("tbl_row_{}", row_idx));
assert_eq!(c.base.id, format!("tbl_row_{}", row_idx));
assert_eq!(c.direction, "row");
assert_eq!(c.children.len(), 2);
}
@@ -666,7 +588,7 @@ mod tests {
// Second child should be a Line
match &container.children[1] {
TemplateElement::Line(l) => {
assert_eq!(l.id, "tbl_header_line");
assert_eq!(l.base.id, "tbl_header_line");
}
_ => panic!("Expected Line separator after header"),
}
@@ -738,14 +660,10 @@ mod tests {
];
let table = RepeatingTableElement {
id: "tbl".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
base: ElementBase::flow(
"tbl".to_string(),
SizeConstraint { width: SizeValue::Fr { value: 1.0 }, ..Default::default() },
),
data_source: ArrayBinding {
path: "items".to_string(),
},
@@ -769,14 +687,14 @@ mod tests {
match &container.children[0] {
TemplateElement::Container(c) => {
let w0 = match &c.children[0] {
TemplateElement::Container(wrap) => match &wrap.size.width {
TemplateElement::Container(wrap) => match &wrap.base.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 {
TemplateElement::Container(wrap) => match &wrap.base.size.width {
SizeValue::Fixed { value } => *value,
_ => panic!("Expected Fixed width for auto column wrapper"),
},
@@ -811,7 +729,7 @@ mod tests {
// Second call — same inputs — cache hit
let result2 = expand_table_cached(&table, &resolved, &mut measurer, 180.0, &mut cache);
assert_eq!(cache.len(), 1); // no new entry
assert_eq!(result1.id, result2.id);
assert_eq!(result1.base.id, result2.base.id);
assert_eq!(result1.children.len(), result2.children.len());
}

View File

@@ -185,7 +185,7 @@ fn collect_break_modes(root: &ContainerElement) -> HashMap<String, String> {
fn collect_break_modes_recursive(el: &TemplateElement, modes: &mut HashMap<String, String>) {
if let TemplateElement::Container(c) = el {
modes.insert(c.id.clone(), c.break_inside.clone());
modes.insert(c.base.id.clone(), c.break_inside.clone());
for child in &c.children {
collect_break_modes_recursive(child, modes);
}
@@ -208,7 +208,7 @@ fn collect_no_repeat_recursive(el: &TemplateElement, set: &mut std::collections:
}
TemplateElement::RepeatingTable(t) => {
if t.repeat_header == Some(false) {
set.insert(t.id.clone());
set.insert(t.base.id.clone());
}
}
_ => {}
@@ -233,7 +233,7 @@ fn build_container(
// 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 {
let container_own_width = match &el.base.size.width {
SizeValue::Fixed { value } => *value,
_ => page_width_mm, // Fr veya Auto ise parent'ın genişliğini kullan
};
@@ -268,17 +268,10 @@ fn build_container(
node_map.insert(
node,
NodeInfo {
element_id: el.id.clone(),
element_type: "container".to_string(),
element_id: el.base.id.clone(),
element_type: el.type_str().to_string(),
content: None,
style: ResolvedStyle {
background_color: el.style.background_color.clone(),
border_color: el.style.border_color.clone(),
border_width: el.style.border_width,
border_radius: el.style.border_radius,
border_style: el.style.border_style.clone(),
..Default::default()
},
style: (&el.style).into(),
children_ids,
},
);
@@ -309,88 +302,63 @@ fn build_element(
page_width_mm,
table_cache,
),
TemplateElement::StaticText(e) => build_text_leaf(
TemplateElement::StaticText(e) => build_resolved_text_leaf(
&e.base,
e.type_str(),
&e.style,
taffy,
node_map,
&e.id,
"static_text",
resolved
.texts
.get(&e.id)
.map(|s| s.as_str())
.unwrap_or(&e.content),
&e.style,
&e.size,
&e.position,
resolved,
parent_direction,
&e.content,
),
TemplateElement::Text(e) => build_resolved_text_leaf(
&e.base,
e.type_str(),
&e.style,
taffy,
node_map,
resolved,
parent_direction,
"",
),
TemplateElement::PageNumber(e) => build_resolved_text_leaf(
&e.base,
e.type_str(),
&e.style,
taffy,
node_map,
resolved,
parent_direction,
"1 / 1",
),
TemplateElement::CurrentDate(e) => build_resolved_text_leaf(
&e.base,
e.type_str(),
&e.style,
taffy,
node_map,
resolved,
parent_direction,
"",
),
TemplateElement::CalculatedText(e) => build_resolved_text_leaf(
&e.base,
e.type_str(),
&e.style,
taffy,
node_map,
resolved,
parent_direction,
"",
),
TemplateElement::Text(e) => {
let text = resolved.texts.get(&e.id).map(|s| s.as_str()).unwrap_or("");
build_text_leaf(
taffy,
node_map,
&e.id,
"text",
text,
&e.style,
&e.size,
&e.position,
parent_direction,
)
}
TemplateElement::PageNumber(e) => {
let text = resolved
.texts
.get(&e.id)
.map(|s| s.as_str())
.unwrap_or("1 / 1");
build_text_leaf(
taffy,
node_map,
&e.id,
"page_number",
text,
&e.style,
&e.size,
&e.position,
parent_direction,
)
}
TemplateElement::CurrentDate(e) => {
let text = resolved.texts.get(&e.id).map(|s| s.as_str()).unwrap_or("");
build_text_leaf(
taffy,
node_map,
&e.id,
"current_date",
text,
&e.style,
&e.size,
&e.position,
parent_direction,
)
}
TemplateElement::CalculatedText(e) => {
let text = resolved.texts.get(&e.id).map(|s| s.as_str()).unwrap_or("");
build_text_leaf(
taffy,
node_map,
&e.id,
"calculated_text",
text,
&e.style,
&e.size,
&e.position,
parent_direction,
)
}
TemplateElement::Line(e) => {
let stroke_w = e.style.stroke_width.unwrap_or(0.5);
let style = sizing::leaf_style(&e.size, &e.position, parent_direction);
let style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction);
// Line: genişlik parent'tan, yükseklik stroke width
let mut leaf_style = style;
if matches!(e.size.height, SizeValue::Auto) {
if matches!(e.base.size.height, SizeValue::Auto) {
leaf_style.size.height = Dimension::length(mm_to_pt(stroke_w));
}
@@ -398,13 +366,13 @@ fn build_element(
node_map.insert(
node,
NodeInfo {
element_id: e.id.clone(),
element_type: "line".to_string(),
element_id: e.base.id.clone(),
element_type: e.type_str().to_string(),
content: Some(ResolvedContent::Line),
style: ResolvedStyle {
stroke_color: e.style.stroke_color.clone(),
stroke_width: Some(stroke_w),
..Default::default()
style: {
let mut s: ResolvedStyle = (&e.style).into();
s.stroke_width = Some(stroke_w);
s
},
children_ids: vec![],
},
@@ -412,37 +380,34 @@ fn build_element(
Ok(node)
}
TemplateElement::Image(e) => {
let style = sizing::leaf_style(&e.size, &e.position, parent_direction);
let src = resolved.images.get(&e.id).cloned().unwrap_or_default();
let style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction);
let src = resolved.images.get(&e.base.id).cloned().unwrap_or_default();
let node = taffy.new_leaf(style)?;
node_map.insert(
node,
NodeInfo {
element_id: e.id.clone(),
element_type: "image".to_string(),
element_id: e.base.id.clone(),
element_type: e.type_str().to_string(),
content: Some(ResolvedContent::Image { src }),
style: ResolvedStyle {
object_fit: e.style.object_fit.clone(),
..Default::default()
},
style: (&e.style).into(),
children_ids: vec![],
},
);
Ok(node)
}
TemplateElement::Barcode(e) => {
let mut style = sizing::leaf_style(&e.size, &e.position, parent_direction);
let value = resolved.barcodes.get(&e.id).cloned().unwrap_or_default();
let mut style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction);
let value = resolved.barcodes.get(&e.base.id).cloned().unwrap_or_default();
// Barcode leaf'e minimum boyut ver (MeasureFunc yok, Auto=0 olur)
let is_qr = e.format == "qr";
let default_h = if is_qr { 20.0 } else { 15.0 }; // mm
let default_w = if is_qr { 20.0 } else { 40.0 }; // mm
if matches!(e.size.height, SizeValue::Auto) {
if matches!(e.base.size.height, SizeValue::Auto) {
style.min_size.height = Dimension::length(mm_to_pt(default_h));
}
if matches!(e.size.width, SizeValue::Auto) {
if matches!(e.base.size.width, SizeValue::Auto) {
style.min_size.width = Dimension::length(mm_to_pt(default_w));
}
@@ -450,17 +415,13 @@ fn build_element(
node_map.insert(
node,
NodeInfo {
element_id: e.id.clone(),
element_type: "barcode".to_string(),
element_id: e.base.id.clone(),
element_type: e.type_str().to_string(),
content: Some(ResolvedContent::Barcode {
format: e.format.clone(),
value,
}),
style: ResolvedStyle {
barcode_color: e.style.color.clone(),
barcode_include_text: e.style.include_text,
..Default::default()
},
style: (&e.style).into(),
children_ids: vec![],
},
);
@@ -497,23 +458,17 @@ fn build_element(
)
}
TemplateElement::Shape(e) => {
let style = sizing::leaf_style(&e.size, &e.position, parent_direction);
let style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction);
let node = taffy.new_leaf(style)?;
node_map.insert(
node,
NodeInfo {
element_id: e.id.clone(),
element_type: "shape".to_string(),
element_id: e.base.id.clone(),
element_type: e.type_str().to_string(),
content: Some(ResolvedContent::Shape {
shape_type: e.shape_type.clone(),
}),
style: ResolvedStyle {
background_color: e.style.background_color.clone(),
border_color: e.style.border_color.clone(),
border_width: e.style.border_width,
border_radius: e.style.border_radius,
..Default::default()
},
style: (&e.style).into(),
children_ids: vec![],
},
);
@@ -522,19 +477,19 @@ fn build_element(
TemplateElement::Checkbox(e) => {
let checked_str = resolved
.texts
.get(&e.id)
.get(&e.base.id)
.map(|s| s.as_str())
.unwrap_or("false");
let checked = checked_str == "true";
let box_size_mm = e.style.size.unwrap_or(4.0);
let style = sizing::leaf_style(&e.size, &e.position, parent_direction);
let style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction);
// Auto size → square based on style.size
let mut leaf_style = style;
if matches!(e.size.width, SizeValue::Auto) {
if matches!(e.base.size.width, SizeValue::Auto) {
leaf_style.size.width = Dimension::length(mm_to_pt(box_size_mm));
}
if matches!(e.size.height, SizeValue::Auto) {
if matches!(e.base.size.height, SizeValue::Auto) {
leaf_style.size.height = Dimension::length(mm_to_pt(box_size_mm));
}
@@ -542,22 +497,17 @@ fn build_element(
node_map.insert(
node,
NodeInfo {
element_id: e.id.clone(),
element_type: "checkbox".to_string(),
element_id: e.base.id.clone(),
element_type: e.type_str().to_string(),
content: Some(ResolvedContent::Checkbox { checked }),
style: ResolvedStyle {
color: e.style.check_color.clone(),
border_color: e.style.border_color.clone(),
border_width: e.style.border_width,
..Default::default()
},
style: (&e.style).into(),
children_ids: vec![],
},
);
Ok(node)
}
TemplateElement::RichText(e) => {
let spans = resolved.rich_texts.get(&e.id).cloned().unwrap_or_default();
let spans = resolved.rich_texts.get(&e.base.id).cloned().unwrap_or_default();
let rich_span_measures: Vec<crate::text_measure::RichSpanMeasure> = spans
.iter()
.map(|s| crate::text_measure::RichSpanMeasure {
@@ -573,7 +523,7 @@ fn build_element(
.map(|s| s.font_size_pt)
.fold(11.0f32, f32::max);
let style = sizing::leaf_style(&e.size, &e.position, parent_direction);
let style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction);
let context = MeasureContext {
text: String::new(),
@@ -600,39 +550,32 @@ fn build_element(
node_map.insert(
node,
NodeInfo {
element_id: e.id.clone(),
element_type: "rich_text".to_string(),
element_id: e.base.id.clone(),
element_type: e.type_str().to_string(),
content: Some(ResolvedContent::RichText {
spans: resolved_spans,
}),
style: ResolvedStyle {
font_size: e.style.font_size,
font_weight: e.style.font_weight.clone(),
font_family: e.style.font_family.clone(),
color: e.style.color.clone(),
text_align: e.style.align.clone(),
..Default::default()
},
style: (&e.style).into(),
children_ids: vec![],
},
);
Ok(node)
}
TemplateElement::Chart(e) => {
let mut style = sizing::leaf_style(&e.size, &e.position, parent_direction);
let mut style = sizing::leaf_style(&e.base.size, &e.base.position, parent_direction);
// Default minimum boyut — Auto ise chart cok kucuk olmasin
if matches!(e.size.width, SizeValue::Auto) {
if matches!(e.base.size.width, SizeValue::Auto) {
style.min_size.width = Dimension::length(mm_to_pt(80.0));
}
if matches!(e.size.height, SizeValue::Auto) {
if matches!(e.base.size.height, SizeValue::Auto) {
style.min_size.height = Dimension::length(mm_to_pt(60.0));
}
let node = taffy.new_leaf(style)?;
node_map.insert(
node,
NodeInfo {
element_id: e.id.clone(),
element_type: "chart".to_string(),
element_id: e.base.id.clone(),
element_type: e.type_str().to_string(),
content: None, // SVG collect_layout'ta uretilecek
style: ResolvedStyle::default(),
children_ids: vec![],
@@ -653,8 +596,8 @@ fn build_element(
node_map.insert(
node,
NodeInfo {
element_id: e.id.clone(),
element_type: "page_break".to_string(),
element_id: e.base.id.clone(),
element_type: e.type_str().to_string(),
content: None,
style: ResolvedStyle::default(),
children_ids: vec![],
@@ -669,7 +612,7 @@ fn build_element(
fn register_expanded_texts(el: &TemplateElement, resolved: &mut ResolvedData) {
match el {
TemplateElement::StaticText(e) => {
resolved.texts.insert(e.id.clone(), e.content.clone());
resolved.texts.insert(e.base.id.clone(), e.content.clone());
}
TemplateElement::Container(e) => {
for child in &e.children {
@@ -680,6 +623,35 @@ fn register_expanded_texts(el: &TemplateElement, resolved: &mut ResolvedData) {
}
}
/// Generic text leaf builder — HasTextStyle trait ile text-benzeri elementleri tek yerde build eder
fn build_resolved_text_leaf(
el_base: &ElementBase,
el_type_str: &str,
text_style: &TextStyle,
taffy: &mut TaffyTree<MeasureContext>,
node_map: &mut HashMap<NodeId, NodeInfo>,
resolved: &ResolvedData,
parent_direction: Option<&str>,
fallback_text: &str,
) -> Result<NodeId, LayoutError> {
let text = resolved
.texts
.get(&el_base.id)
.map(|s| s.as_str())
.unwrap_or(fallback_text);
build_text_leaf(
taffy,
node_map,
&el_base.id,
el_type_str,
text,
text_style,
&el_base.size,
&el_base.position,
parent_direction,
)
}
/// Text leaf node oluştur (static_text, text, page_number için ortak)
#[allow(clippy::too_many_arguments)]
fn build_text_leaf(
@@ -714,15 +686,7 @@ fn build_text_leaf(
content: Some(ResolvedContent::Text {
value: text.to_string(),
}),
style: ResolvedStyle {
font_size: text_style.font_size,
font_weight: text_style.font_weight.clone(),
font_style: text_style.font_style.clone(),
font_family: text_style.font_family.clone(),
color: text_style.color.clone(),
text_align: text_style.align.clone(),
..Default::default()
},
style: text_style.into(),
children_ids: vec![],
},
);
@@ -902,17 +866,14 @@ mod tests {
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
base: ElementBase::flow("root".to_string(), SizeConstraint {
width: SizeValue::Auto,
height: SizeValue::Auto,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
},
}),
direction: "column".to_string(),
gap: 5.0,
padding: Padding {
@@ -927,17 +888,14 @@ mod tests {
break_inside: "auto".to_string(),
children: vec![
TemplateElement::StaticText(StaticTextElement {
id: "title".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
base: ElementBase::flow("title".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
},
}),
style: TextStyle {
font_size: Some(18.0),
font_weight: Some("bold".to_string()),
@@ -946,34 +904,28 @@ mod tests {
content: "FATURA".to_string(),
}),
TemplateElement::Line(LineElement {
id: "line1".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
base: ElementBase::flow("line1".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
},
}),
style: LineStyle {
stroke_color: Some("#000000".to_string()),
stroke_width: Some(0.5),
},
}),
TemplateElement::StaticText(StaticTextElement {
id: "body".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
base: ElementBase::flow("body".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
},
}),
style: TextStyle {
font_size: Some(11.0),
..Default::default()
@@ -1051,17 +1003,14 @@ mod tests {
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
base: ElementBase::flow("root".to_string(), SizeConstraint {
width: SizeValue::Auto,
height: SizeValue::Auto,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
},
}),
direction: "column".to_string(),
gap: 0.0,
padding: Padding {
@@ -1075,17 +1024,14 @@ mod tests {
style: ContainerStyle::default(),
break_inside: "auto".to_string(),
children: vec![TemplateElement::Container(ContainerElement {
id: "row".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
base: ElementBase::flow("row".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
},
}),
direction: "row".to_string(),
gap: 5.0,
padding: Padding {
@@ -1100,17 +1046,14 @@ mod tests {
break_inside: "auto".to_string(),
children: vec![
TemplateElement::StaticText(StaticTextElement {
id: "left".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
base: ElementBase::flow("left".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
},
}),
style: TextStyle {
font_size: Some(11.0),
..Default::default()
@@ -1118,17 +1061,14 @@ mod tests {
content: "Sol".to_string(),
}),
TemplateElement::StaticText(StaticTextElement {
id: "right".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
base: ElementBase::flow("right".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
},
}),
style: TextStyle {
font_size: Some(11.0),
..Default::default()
@@ -1184,17 +1124,14 @@ mod tests {
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
base: ElementBase::flow("root".to_string(), SizeConstraint {
width: SizeValue::Auto,
height: SizeValue::Auto,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
},
}),
direction: "column".to_string(),
gap: 0.0,
padding: Padding {
@@ -1208,16 +1145,18 @@ mod tests {
style: ContainerStyle::default(),
break_inside: "auto".to_string(),
children: vec![TemplateElement::StaticText(StaticTextElement {
id: "abs_text".to_string(),
condition: None,
position: PositionMode::Absolute { x: 50.0, y: 80.0 },
size: SizeConstraint {
width: SizeValue::Fixed { value: 100.0 },
height: SizeValue::Auto,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
base: ElementBase {
id: "abs_text".to_string(),
condition: None,
position: PositionMode::Absolute { x: 50.0, y: 80.0 },
size: SizeConstraint {
width: SizeValue::Fixed { value: 100.0 },
height: SizeValue::Auto,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
},
},
style: TextStyle {
font_size: Some(14.0),
@@ -1276,10 +1215,7 @@ mod tests {
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: sz_auto.clone(),
base: ElementBase::flow("root".to_string(), sz_auto.clone()),
direction: "column".to_string(),
gap: 5.0,
padding: Padding {
@@ -1295,10 +1231,7 @@ mod tests {
children: vec![
// Header row
TemplateElement::Container(ContainerElement {
id: "c_header".to_string(),
condition: None,
position: PositionMode::Flow,
size: sz_fr_auto.clone(),
base: ElementBase::flow("c_header".to_string(), sz_fr_auto.clone()),
direction: "row".to_string(),
gap: 5.0,
padding: p0.clone(),
@@ -1309,10 +1242,7 @@ mod tests {
children: vec![
// Sol: firma bilgileri
TemplateElement::Container(ContainerElement {
id: "c_firma".to_string(),
condition: None,
position: PositionMode::Flow,
size: sz_fr_auto.clone(),
base: ElementBase::flow("c_firma".to_string(), sz_fr_auto.clone()),
direction: "column".to_string(),
gap: 1.0,
padding: p0.clone(),
@@ -1322,10 +1252,7 @@ mod tests {
break_inside: "auto".to_string(),
children: vec![
TemplateElement::StaticText(StaticTextElement {
id: "el_firma_unvan".to_string(),
condition: None,
position: PositionMode::Flow,
size: sz_auto.clone(),
base: ElementBase::flow("el_firma_unvan".to_string(), sz_auto.clone()),
style: TextStyle {
font_size: Some(14.0),
font_weight: Some("bold".to_string()),
@@ -1334,10 +1261,7 @@ mod tests {
content: "Teknova Yazılım ve Danışmanlık A.Ş.".to_string(),
}),
TemplateElement::StaticText(StaticTextElement {
id: "el_firma_adres".to_string(),
condition: None,
position: PositionMode::Flow,
size: sz_auto.clone(),
base: ElementBase::flow("el_firma_adres".to_string(), sz_auto.clone()),
style: TextStyle {
font_size: Some(9.0),
..Default::default()
@@ -1346,10 +1270,7 @@ mod tests {
.to_string(),
}),
TemplateElement::StaticText(StaticTextElement {
id: "el_firma_il".to_string(),
condition: None,
position: PositionMode::Flow,
size: sz_auto.clone(),
base: ElementBase::flow("el_firma_il".to_string(), sz_auto.clone()),
style: TextStyle {
font_size: Some(9.0),
..Default::default()
@@ -1357,10 +1278,7 @@ mod tests {
content: "Istanbul".to_string(),
}),
TemplateElement::StaticText(StaticTextElement {
id: "el_firma_tel".to_string(),
condition: None,
position: PositionMode::Flow,
size: sz_auto.clone(),
base: ElementBase::flow("el_firma_tel".to_string(), sz_auto.clone()),
style: TextStyle {
font_size: Some(9.0),
..Default::default()
@@ -1368,10 +1286,7 @@ mod tests {
content: "Tel: +90 212 555 0042".to_string(),
}),
TemplateElement::StaticText(StaticTextElement {
id: "el_firma_vd".to_string(),
condition: None,
position: PositionMode::Flow,
size: sz_auto.clone(),
base: ElementBase::flow("el_firma_vd".to_string(), sz_auto.clone()),
style: TextStyle {
font_size: Some(9.0),
..Default::default()
@@ -1379,10 +1294,7 @@ mod tests {
content: "VD: Levent VD".to_string(),
}),
TemplateElement::StaticText(StaticTextElement {
id: "el_firma_vn".to_string(),
condition: None,
position: PositionMode::Flow,
size: sz_auto.clone(),
base: ElementBase::flow("el_firma_vn".to_string(), sz_auto.clone()),
style: TextStyle {
font_size: Some(9.0),
..Default::default()
@@ -1393,10 +1305,7 @@ mod tests {
}),
// Sağ: fatura başlığı
TemplateElement::Container(ContainerElement {
id: "c_fatura_baslik".to_string(),
condition: None,
position: PositionMode::Flow,
size: sz_auto.clone(),
base: ElementBase::flow("c_fatura_baslik".to_string(), sz_auto.clone()),
direction: "column".to_string(),
gap: 2.0,
padding: p0.clone(),
@@ -1406,10 +1315,7 @@ mod tests {
break_inside: "auto".to_string(),
children: vec![
TemplateElement::StaticText(StaticTextElement {
id: "el_fatura_baslik".to_string(),
condition: None,
position: PositionMode::Flow,
size: sz_auto.clone(),
base: ElementBase::flow("el_fatura_baslik".to_string(), sz_auto.clone()),
style: TextStyle {
font_size: Some(18.0),
font_weight: Some("bold".to_string()),
@@ -1418,10 +1324,7 @@ mod tests {
content: "FATURA".to_string(),
}),
TemplateElement::StaticText(StaticTextElement {
id: "el_fatura_no".to_string(),
condition: None,
position: PositionMode::Flow,
size: sz_auto.clone(),
base: ElementBase::flow("el_fatura_no".to_string(), sz_auto.clone()),
style: TextStyle {
font_size: Some(10.0),
..Default::default()
@@ -1429,10 +1332,7 @@ mod tests {
content: "No: FTR-2026-001547".to_string(),
}),
TemplateElement::StaticText(StaticTextElement {
id: "el_fatura_tarih".to_string(),
condition: None,
position: PositionMode::Flow,
size: sz_auto.clone(),
base: ElementBase::flow("el_fatura_tarih".to_string(), sz_auto.clone()),
style: TextStyle {
font_size: Some(10.0),
..Default::default()
@@ -1440,10 +1340,7 @@ mod tests {
content: "Tarih: 2026-03-29".to_string(),
}),
TemplateElement::StaticText(StaticTextElement {
id: "el_fatura_vade".to_string(),
condition: None,
position: PositionMode::Flow,
size: sz_auto.clone(),
base: ElementBase::flow("el_fatura_vade".to_string(), sz_auto.clone()),
style: TextStyle {
font_size: Some(10.0),
..Default::default()

View File

@@ -27,12 +27,9 @@ fn base_template() -> Template {
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("root".to_string(), SizeConstraint::default()),
direction: "column".to_string(),
gap: 5.0,
condition: None,
padding: Padding {
top: 15.0,
right: 15.0,
@@ -57,14 +54,11 @@ fn test_1_2_text_wrapping_layout_height() {
// Dar bir container'da uzun metin → yükseklik tek satırdan fazla olmalı
let mut tpl = base_template();
tpl.root.children.push(TemplateElement::StaticText(StaticTextElement {
id: "long_text".to_string(),
position: PositionMode::Flow,
condition: None,
size: SizeConstraint {
base: ElementBase::flow("long_text".to_string(), SizeConstraint {
width: SizeValue::Fixed { value: 40.0 }, // 40mm genişlik — kısa
height: SizeValue::Auto,
..Default::default()
},
}),
style: TextStyle {
font_size: Some(12.0),
..Default::default()
@@ -94,14 +88,11 @@ fn test_1_2_text_wrapping_pdf_renders() {
// PDF render sırasında text wrapping çalışmalı — crash olmamalı
let mut tpl = base_template();
tpl.root.children.push(TemplateElement::StaticText(StaticTextElement {
id: "wrap_pdf".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
base: ElementBase::flow("wrap_pdf".to_string(), SizeConstraint {
width: SizeValue::Fixed { value: 50.0 },
height: SizeValue::Auto,
..Default::default()
},
}),
style: TextStyle {
font_size: Some(11.0),
..Default::default()
@@ -125,14 +116,11 @@ fn test_1_2_text_wrapping_pdf_renders() {
fn test_1_3_image_object_fit_in_layout() {
let mut tpl = base_template();
tpl.root.children.push(TemplateElement::Image(ImageElement {
id: "img_contain".to_string(),
position: PositionMode::Flow,
condition: None,
size: SizeConstraint {
base: ElementBase::flow("img_contain".to_string(), SizeConstraint {
width: SizeValue::Fixed { value: 40.0 },
height: SizeValue::Fixed { value: 30.0 },
..Default::default()
},
}),
src: Some("data:image/png;base64,iVBORw0KGgo=".to_string()),
binding: None,
style: ImageStyle {
@@ -167,14 +155,11 @@ fn test_1_4_italic_font_in_pdf() {
tpl.root
.children
.push(TemplateElement::StaticText(StaticTextElement {
id: "italic_text".to_string(),
position: PositionMode::Flow,
condition: None,
size: SizeConstraint {
base: ElementBase::flow("italic_text".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
}),
style: TextStyle {
font_size: Some(12.0),
font_style: Some("italic".to_string()),
@@ -205,14 +190,11 @@ fn test_1_4_bold_italic_font_in_pdf() {
tpl.root
.children
.push(TemplateElement::StaticText(StaticTextElement {
id: "bold_italic".to_string(),
position: PositionMode::Flow,
condition: None,
size: SizeConstraint {
base: ElementBase::flow("bold_italic".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
}),
style: TextStyle {
font_size: Some(14.0),
font_weight: Some("bold".to_string()),
@@ -239,14 +221,11 @@ fn test_2_1_repeat_header_false_no_repeat_on_second_page() {
tpl.root
.children
.push(TemplateElement::RepeatingTable(RepeatingTableElement {
id: "tbl_no_repeat".to_string(),
position: PositionMode::Flow,
condition: None,
size: SizeConstraint {
base: ElementBase::flow("tbl_no_repeat".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
}),
data_source: ArrayBinding {
path: "items".to_string(),
},
@@ -305,14 +284,11 @@ fn test_2_1_repeat_header_true_repeats_on_second_page() {
tpl.root
.children
.push(TemplateElement::RepeatingTable(RepeatingTableElement {
id: "tbl_repeat".to_string(),
position: PositionMode::Flow,
condition: None,
size: SizeConstraint {
base: ElementBase::flow("tbl_repeat".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
}),
data_source: ArrayBinding {
path: "items".to_string(),
},
@@ -389,14 +365,11 @@ fn test_2_2_table_column_format_currency() {
tpl.root
.children
.push(TemplateElement::RepeatingTable(RepeatingTableElement {
id: "tbl_fmt".to_string(),
position: PositionMode::Flow,
condition: None,
size: SizeConstraint {
base: ElementBase::flow("tbl_fmt".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
}),
data_source: ArrayBinding {
path: "items".to_string(),
},
@@ -459,14 +432,11 @@ fn test_2_2_table_column_format_currency() {
fn test_2_3_rounded_rectangle_renders() {
let mut tpl = base_template();
tpl.root.children.push(TemplateElement::Shape(ShapeElement {
id: "rounded_shape".to_string(),
position: PositionMode::Flow,
condition: None,
size: SizeConstraint {
base: ElementBase::flow("rounded_shape".to_string(), SizeConstraint {
width: SizeValue::Fixed { value: 50.0 },
height: SizeValue::Fixed { value: 30.0 },
..Default::default()
},
}),
shape_type: "rounded_rectangle".to_string(),
style: ContainerStyle {
background_color: Some("#3b82f6".to_string()),
@@ -505,14 +475,11 @@ fn test_2_3_container_border_radius_renders() {
tpl.root
.children
.push(TemplateElement::StaticText(StaticTextElement {
id: "text_in_rounded".to_string(),
position: PositionMode::Flow,
condition: None,
size: SizeConstraint {
base: ElementBase::flow("text_in_rounded".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
}),
style: TextStyle {
font_size: Some(12.0),
..Default::default()
@@ -604,14 +571,11 @@ fn test_2_7_format_config_in_template() {
fn test_ellipse_shape_renders() {
let mut tpl = base_template();
tpl.root.children.push(TemplateElement::Shape(ShapeElement {
id: "ellipse".to_string(),
position: PositionMode::Flow,
condition: None,
size: SizeConstraint {
base: ElementBase::flow("ellipse".to_string(), SizeConstraint {
width: SizeValue::Fixed { value: 40.0 },
height: SizeValue::Fixed { value: 20.0 },
..Default::default()
},
}),
shape_type: "ellipse".to_string(),
style: ContainerStyle {
background_color: Some("#ff6600".to_string()),
@@ -635,22 +599,21 @@ fn test_ellipse_shape_renders() {
fn test_7_1_condition_gt_hides_element() {
let mut tpl = base_template();
tpl.root.children.push(TemplateElement::StaticText(StaticTextElement {
id: "always_visible".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("always_visible".to_string(), SizeConstraint::default()),
style: TextStyle { font_size: Some(10.0), ..Default::default() },
content: "Visible".to_string(),
}));
tpl.root.children.push(TemplateElement::Text(TextElement {
id: "conditional_text".to_string(),
condition: Some(Condition {
path: "toplamlar.iskonto".to_string(),
operator: "gt".to_string(),
value: Some(serde_json::json!(0)),
}),
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase {
id: "conditional_text".to_string(),
condition: Some(Condition {
path: "toplamlar.iskonto".to_string(),
operator: "gt".to_string(),
value: Some(serde_json::json!(0)),
}),
position: PositionMode::Flow,
size: SizeConstraint::default(),
},
style: TextStyle { font_size: Some(10.0), ..Default::default() },
content: None,
binding: ScalarBinding { path: "toplamlar.iskonto".to_string() },
@@ -676,14 +639,16 @@ fn test_7_1_condition_gt_hides_element() {
fn test_7_1_condition_gt_shows_element() {
let mut tpl = base_template();
tpl.root.children.push(TemplateElement::Text(TextElement {
id: "conditional_text".to_string(),
condition: Some(Condition {
path: "toplamlar.iskonto".to_string(),
operator: "gt".to_string(),
value: Some(serde_json::json!(0)),
}),
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase {
id: "conditional_text".to_string(),
condition: Some(Condition {
path: "toplamlar.iskonto".to_string(),
operator: "gt".to_string(),
value: Some(serde_json::json!(0)),
}),
position: PositionMode::Flow,
size: SizeConstraint::default(),
},
style: TextStyle { font_size: Some(10.0), ..Default::default() },
content: None,
binding: ScalarBinding { path: "toplamlar.iskonto".to_string() },
@@ -705,14 +670,16 @@ fn test_7_1_condition_gt_shows_element() {
fn test_7_1_condition_eq_operator() {
let mut tpl = base_template();
tpl.root.children.push(TemplateElement::StaticText(StaticTextElement {
id: "status_text".to_string(),
condition: Some(Condition {
path: "durum".to_string(),
operator: "eq".to_string(),
value: Some(serde_json::json!("aktif")),
}),
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase {
id: "status_text".to_string(),
condition: Some(Condition {
path: "durum".to_string(),
operator: "eq".to_string(),
value: Some(serde_json::json!("aktif")),
}),
position: PositionMode::Flow,
size: SizeConstraint::default(),
},
style: TextStyle { font_size: Some(10.0), ..Default::default() },
content: "Aktif".to_string(),
}));
@@ -732,14 +699,16 @@ fn test_7_1_condition_eq_operator() {
fn test_7_1_condition_empty_not_empty() {
let mut tpl = base_template();
tpl.root.children.push(TemplateElement::StaticText(StaticTextElement {
id: "show_if_exists".to_string(),
condition: Some(Condition {
path: "note".to_string(),
operator: "not_empty".to_string(),
value: None,
}),
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase {
id: "show_if_exists".to_string(),
condition: Some(Condition {
path: "note".to_string(),
operator: "not_empty".to_string(),
value: None,
}),
position: PositionMode::Flow,
size: SizeConstraint::default(),
},
style: TextStyle { font_size: Some(10.0), ..Default::default() },
content: "Has note".to_string(),
}));
@@ -763,14 +732,16 @@ fn test_7_1_condition_empty_not_empty() {
fn test_7_1_condition_on_container_hides_children() {
let mut tpl = base_template();
tpl.root.children.push(TemplateElement::Container(ContainerElement {
id: "cond_container".to_string(),
condition: Some(Condition {
path: "show".to_string(),
operator: "eq".to_string(),
value: Some(serde_json::json!(true)),
}),
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase {
id: "cond_container".to_string(),
condition: Some(Condition {
path: "show".to_string(),
operator: "eq".to_string(),
value: Some(serde_json::json!(true)),
}),
position: PositionMode::Flow,
size: SizeConstraint::default(),
},
direction: "column".to_string(),
gap: 0.0,
padding: Padding::default(),
@@ -779,10 +750,7 @@ fn test_7_1_condition_on_container_hides_children() {
style: ContainerStyle::default(),
break_inside: "auto".to_string(),
children: vec![TemplateElement::StaticText(StaticTextElement {
id: "child_text".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("child_text".to_string(), SizeConstraint::default()),
style: TextStyle { font_size: Some(10.0), ..Default::default() },
content: "Child".to_string(),
})],
@@ -854,10 +822,7 @@ fn test_7_5_effective_format_config_priority() {
header: None,
footer: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("root".to_string(), SizeConstraint::default()),
direction: "column".to_string(),
gap: 0.0,
padding: Padding::default(),
@@ -889,10 +854,7 @@ fn test_7_5_effective_format_config_locale_fallback() {
header: None,
footer: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("root".to_string(), SizeConstraint::default()),
direction: "column".to_string(),
gap: 0.0,
padding: Padding::default(),
@@ -915,14 +877,11 @@ fn test_7_5_locale_affects_table_currency_format() {
let mut tpl = base_template();
tpl.locale = Some("en-US".to_string());
tpl.root.children.push(TemplateElement::RepeatingTable(RepeatingTableElement {
id: "tbl_locale".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
base: ElementBase::flow("tbl_locale".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
}),
data_source: ArrayBinding { path: "items".to_string() },
columns: vec![
TableColumn {

View File

@@ -20,10 +20,7 @@ fn simple_template() -> Template {
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("root".to_string(), SizeConstraint::default()),
direction: "column".to_string(),
gap: 5.0,
padding: Padding {
@@ -37,14 +34,14 @@ fn simple_template() -> Template {
style: ContainerStyle::default(),
break_inside: "auto".to_string(),
children: vec![TemplateElement::StaticText(StaticTextElement {
id: "title".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
base: ElementBase::flow(
"title".to_string(),
SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
),
style: TextStyle {
font_size: Some(14.0),
font_weight: Some("bold".to_string()),
@@ -166,10 +163,7 @@ fn test_compute_layout_with_data_binding() {
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("root".to_string(), SizeConstraint::default()),
direction: "column".to_string(),
gap: 0.0,
padding: Padding {
@@ -183,14 +177,14 @@ fn test_compute_layout_with_data_binding() {
style: ContainerStyle::default(),
break_inside: "auto".to_string(),
children: vec![TemplateElement::Text(TextElement {
id: "bound_text".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
base: ElementBase::flow(
"bound_text".to_string(),
SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
),
style: TextStyle {
font_size: Some(12.0),
..Default::default()
@@ -235,10 +229,7 @@ fn test_compute_layout_multiple_children_ordering() {
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("root".to_string(), SizeConstraint::default()),
direction: "column".to_string(),
gap: 5.0,
padding: Padding {
@@ -253,14 +244,14 @@ fn test_compute_layout_multiple_children_ordering() {
break_inside: "auto".to_string(),
children: vec![
TemplateElement::StaticText(StaticTextElement {
id: "first".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
base: ElementBase::flow(
"first".to_string(),
SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
),
style: TextStyle {
font_size: Some(12.0),
..Default::default()
@@ -268,14 +259,14 @@ fn test_compute_layout_multiple_children_ordering() {
content: "First".to_string(),
}),
TemplateElement::StaticText(StaticTextElement {
id: "second".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
base: ElementBase::flow(
"second".to_string(),
SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
),
style: TextStyle {
font_size: Some(12.0),
..Default::default()

View File

@@ -23,10 +23,7 @@ fn simple_template() -> Template {
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("root".to_string(), SizeConstraint::default()),
direction: "column".to_string(),
gap: 5.0,
padding: Padding {
@@ -40,14 +37,11 @@ fn simple_template() -> Template {
style: ContainerStyle::default(),
break_inside: "auto".to_string(),
children: vec![TemplateElement::StaticText(StaticTextElement {
id: "title".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
base: ElementBase::flow("title".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
}),
style: TextStyle {
font_size: Some(18.0),
font_weight: Some("bold".to_string()),
@@ -94,10 +88,7 @@ fn test_render_pdf_with_multiple_elements() {
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("root".to_string(), SizeConstraint::default()),
direction: "column".to_string(),
gap: 5.0,
padding: Padding {
@@ -112,14 +103,11 @@ fn test_render_pdf_with_multiple_elements() {
break_inside: "auto".to_string(),
children: vec![
TemplateElement::StaticText(StaticTextElement {
id: "header".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
base: ElementBase::flow("header".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
}),
style: TextStyle {
font_size: Some(16.0),
font_weight: Some("bold".to_string()),
@@ -128,28 +116,22 @@ fn test_render_pdf_with_multiple_elements() {
content: "FATURA".to_string(),
}),
TemplateElement::Line(LineElement {
id: "sep".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
base: ElementBase::flow("sep".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
}),
style: LineStyle {
stroke_color: Some("#000000".to_string()),
stroke_width: Some(0.5),
},
}),
TemplateElement::StaticText(StaticTextElement {
id: "body".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
base: ElementBase::flow("body".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
}),
style: TextStyle {
font_size: Some(11.0),
..Default::default()
@@ -192,10 +174,7 @@ fn test_render_pdf_with_container_styles() {
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("root".to_string(), SizeConstraint::default()),
direction: "column".to_string(),
gap: 0.0,
padding: Padding {
@@ -214,14 +193,11 @@ fn test_render_pdf_with_container_styles() {
},
break_inside: "auto".to_string(),
children: vec![TemplateElement::StaticText(StaticTextElement {
id: "text".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
base: ElementBase::flow("text".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
}),
style: TextStyle {
font_size: Some(12.0),
color: Some("#ff0000".to_string()),
@@ -257,10 +233,7 @@ fn test_page_break_produces_multiple_pages() {
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
base: ElementBase::flow("root".to_string(), SizeConstraint::default()),
direction: "column".to_string(),
gap: 5.0,
padding: Padding {
@@ -275,14 +248,11 @@ fn test_page_break_produces_multiple_pages() {
break_inside: "auto".to_string(),
children: vec![
TemplateElement::StaticText(StaticTextElement {
id: "t1".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
base: ElementBase::flow("t1".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
}),
style: TextStyle {
font_size: Some(18.0),
..Default::default()
@@ -290,18 +260,14 @@ fn test_page_break_produces_multiple_pages() {
content: "Page 1 content".to_string(),
}),
TemplateElement::PageBreak(PageBreakElement {
id: "pb1".to_string(),
condition: None,
base: ElementBase::flow("pb1".to_string(), SizeConstraint::default()),
}),
TemplateElement::StaticText(StaticTextElement {
id: "t2".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint {
base: ElementBase::flow("t2".to_string(), SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
}),
style: TextStyle {
font_size: Some(18.0),
..Default::default()