Files
dreport/layout-engine/src/data_resolve.rs
2026-04-07 02:55:16 +03:00

842 lines
30 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use dreport_core::models::*;
use serde_json::Value;
use std::collections::HashMap;
/// Ş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.
fn format_current_date(fmt: &str) -> String {
let (year, month, day, hour, minute, second) = current_datetime_parts();
fmt.replace("YYYY", &format!("{:04}", year))
.replace("MM", &format!("{:02}", month))
.replace("DD", &format!("{:02}", day))
.replace("HH", &format!("{:02}", hour))
.replace("mm", &format!("{:02}", minute))
.replace("ss", &format!("{:02}", second))
}
#[cfg(target_arch = "wasm32")]
fn current_datetime_parts() -> (i32, u32, u32, u32, u32, u32) {
let d = js_sys::Date::new_0();
(
d.get_full_year() as i32,
d.get_month() as u32 + 1, // JS months are 0-based
d.get_date() as u32,
d.get_hours() as u32,
d.get_minutes() as u32,
d.get_seconds() as u32,
)
}
#[cfg(not(target_arch = "wasm32"))]
fn current_datetime_parts() -> (i32, u32, u32, u32, u32, u32) {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
// Simple UTC date calculation (no timezone dependency)
let days = (secs / 86400) as i64;
let time_of_day = secs % 86400;
let hour = (time_of_day / 3600) as u32;
let minute = ((time_of_day % 3600) / 60) as u32;
let second = (time_of_day % 60) as u32;
// Days since 1970-01-01 → year/month/day (civil calendar)
// Algorithm from Howard Hinnant's chrono-compatible date library
let z = days + 719468;
let era = (if z >= 0 { z } else { z - 146096 }) / 146097;
let doe = (z - era * 146097) as u32;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y as i32, m, d, hour, minute, second)
}
/// Çözümlenmiş rich text span'ı
#[derive(Debug, Clone)]
pub struct ResolvedRichSpan {
pub text: String,
pub font_size: Option<f64>,
pub font_weight: Option<String>,
pub font_family: Option<String>,
pub color: Option<String>,
}
/// Çözümlenmiş chart verisi
#[derive(Debug, Clone)]
pub struct ResolvedChartData {
pub chart_type: ChartType,
pub categories: Vec<String>,
pub series: Vec<ChartSeries>,
pub title: Option<ChartTitle>,
pub legend: Option<ChartLegend>,
pub labels: Option<ChartLabels>,
pub axis: Option<ChartAxis>,
pub style: ChartStyle,
pub group_mode: Option<GroupMode>,
}
#[derive(Debug, Clone)]
pub struct ChartSeries {
pub name: String,
pub values: Vec<f64>,
}
/// Her element ID'si için çözümlenmiş text içeriğini tutar.
/// Table ve barcode gibi özel tipler de burada çözülür.
#[derive(Debug, Clone)]
pub struct ResolvedData {
/// element_id → çözümlenmiş text içeriği
pub texts: HashMap<String, String>,
/// element_id → çözümlenmiş tablo verileri (headers, rows)
pub tables: HashMap<String, ResolvedTable>,
/// element_id → çözümlenmiş barcode değeri
pub barcodes: HashMap<String, String>,
/// element_id → çözümlenmiş image src
pub images: HashMap<String, String>,
/// page_number element_id → format string (sayfa bölme sonrası çözülecek)
pub page_number_formats: HashMap<String, String>,
/// element_id → çözümlenmiş rich text span listesi
pub rich_texts: HashMap<String, Vec<ResolvedRichSpan>>,
/// element_id → çözümlenmiş chart verisi
pub charts: HashMap<String, ResolvedChartData>,
/// Koşulu sağlamayan (gizlenmesi gereken) element ID'leri
pub hidden_elements: std::collections::HashSet<String>,
}
#[derive(Debug, Clone)]
pub struct ResolvedTable {
pub rows: Vec<Vec<String>>,
}
/// JSON path ile veri çek: "firma.unvan" → data["firma"]["unvan"]
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
}
/// JSON Value → display string
fn value_to_string(v: &Value) -> String {
match v {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
Value::Null => String::new(),
_ => v.to_string(),
}
}
/// Template'deki tüm binding'leri çözümle.
pub fn resolve_template(template: &Template, data: &Value) -> ResolvedData {
let mut resolved = ResolvedData {
texts: HashMap::new(),
tables: HashMap::new(),
barcodes: HashMap::new(),
images: HashMap::new(),
page_number_formats: HashMap::new(),
rich_texts: HashMap::new(),
charts: HashMap::new(),
hidden_elements: std::collections::HashSet::new(),
};
let fc = template.effective_format_config();
if let Some(ref header) = template.header {
resolve_element(
&TemplateElement::Container(header.clone()),
data,
&mut resolved,
&fc,
);
}
resolve_element(
&TemplateElement::Container(template.root.clone()),
data,
&mut resolved,
&fc,
);
if let Some(ref footer) = template.footer {
resolve_element(
&TemplateElement::Container(footer.clone()),
data,
&mut resolved,
&fc,
);
}
resolved
}
/// Koşul değerlendirme: Condition struct'ındaki path, operator, value ile data'yı karşılaştır.
fn evaluate_condition(condition: &dreport_core::models::Condition, data: &Value) -> bool {
let actual = resolve_path(data, &condition.path);
match condition.operator.as_str() {
"empty" => matches!(actual, Value::Null) || actual.as_str().is_some_and(|s| s.is_empty()),
"not_empty" => !matches!(actual, Value::Null) && !actual.as_str().is_some_and(|s| s.is_empty()),
"eq" => {
if let Some(ref expected) = condition.value {
json_values_eq(actual, expected)
} else {
actual.is_null()
}
}
"neq" => {
if let Some(ref expected) = condition.value {
!json_values_eq(actual, expected)
} else {
!actual.is_null()
}
}
op @ ("gt" | "gte" | "lt" | "lte") => {
let a = actual.as_f64().unwrap_or(0.0);
let b = condition.value.as_ref().and_then(|v| v.as_f64()).unwrap_or(0.0);
match op {
"gt" => a > b,
"gte" => a >= b,
"lt" => a < b,
"lte" => a <= b,
_ => unreachable!(),
}
}
_ => true, // bilinmeyen operator → göster
}
}
/// İki JSON değerini karşılaştır (tip dönüşümlü).
fn json_values_eq(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Number(a), Value::Number(b)) => a.as_f64() == b.as_f64(),
(Value::String(a), Value::String(b)) => a == b,
(Value::Bool(a), Value::Bool(b)) => a == b,
(Value::Null, Value::Null) => true,
// Çapraz tip karşılaştırma: sayı string vs sayı
(Value::String(s), Value::Number(n)) | (Value::Number(n), Value::String(s)) => {
s.parse::<f64>().ok() == n.as_f64()
}
_ => a == b,
}
}
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) {
resolved.hidden_elements.insert(el.id().to_string());
return;
}
match el {
TemplateElement::StaticText(e) => {
resolved.texts.insert(e.id.clone(), e.content.clone());
}
TemplateElement::Text(e) => {
let bound_value = value_to_string(resolve_path(data, &e.binding.path));
let text = match &e.content {
Some(prefix) if !prefix.is_empty() => format!("{}{}", prefix, bound_value),
_ => bound_value,
};
resolved.texts.insert(e.id.clone(), text);
}
TemplateElement::PageNumber(e) => {
// Format string'i sakla — sayfa bölme sonrası gerçek değerlerle çözülecek
let fmt = e
.format
.as_deref()
.unwrap_or("{current} / {total}")
.to_string();
resolved
.page_number_formats
.insert(e.id.clone(), fmt.clone());
// Placeholder koy (tek sayfalık fallback)
resolved.texts.insert(
e.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);
}
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);
}
TemplateElement::RepeatingTable(e) => {
let array = resolve_path(data, &e.data_source.path);
let rows = match array {
Value::Array(items) => {
items
.iter()
.map(|item| {
e.columns
.iter()
.map(|col| {
let v = resolve_path(item, &col.field);
let raw = value_to_string(v);
// Sütun formatı varsa uygula (currency, percentage, number, date)
if let Some(ref fmt) = col.format {
crate::expr_eval::apply_format_with_config(&raw, Some(fmt.as_str()), format_config)
} else {
raw
}
})
.collect()
})
.collect()
}
_ => vec![],
};
resolved.tables.insert(e.id.clone(), ResolvedTable { rows });
}
TemplateElement::Container(e) => {
for child in &e.children {
resolve_element(child, data, resolved, format_config);
}
}
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);
}
TemplateElement::Checkbox(e) => {
let checked = if let Some(binding) = &e.binding {
let val = resolve_path(data, &binding.path);
match val {
Value::Bool(b) => *b,
Value::Number(n) => n.as_f64().unwrap_or(0.0) != 0.0,
Value::String(s) => s == "true" || s == "1",
_ => false,
}
} else {
e.checked.unwrap_or(false)
};
// Store as "true"/"false" string in texts map
resolved.texts.insert(e.id.clone(), checked.to_string());
}
TemplateElement::CalculatedText(e) => {
let result = crate::expr_eval::evaluate_expression(&e.expression, data);
let formatted = crate::expr_eval::apply_format_with_config(&result, e.format.as_deref(), format_config);
// 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
.content
.iter()
.map(|span| {
let text = if let Some(ref binding) = span.binding {
let bound = value_to_string(resolve_path(data, &binding.path));
match &span.text {
Some(prefix) if !prefix.is_empty() => format!("{}{}", prefix, bound),
_ => bound,
}
} else {
span.text.clone().unwrap_or_default()
};
ResolvedRichSpan {
text,
font_size: span.style.font_size.or(e.style.font_size),
font_weight: span
.style
.font_weight
.clone()
.or(e.style.font_weight.clone()),
font_family: span
.style
.font_family
.clone()
.or(e.style.font_family.clone()),
color: span.style.color.clone().or(e.style.color.clone()),
}
})
.collect();
resolved.rich_texts.insert(e.id.clone(), spans);
}
TemplateElement::Chart(e) => {
let array = resolve_path(data, &e.data_source.path);
let chart_data = match array {
Value::Array(items) if !items.is_empty() => resolve_chart_data(e, items),
_ => ResolvedChartData {
chart_type: e.chart_type.clone(),
categories: vec![],
series: vec![],
title: e.title.clone(),
legend: e.legend.clone(),
labels: e.labels.clone(),
axis: e.axis.clone(),
style: e.style.clone(),
group_mode: e.group_mode.clone(),
},
};
resolved.charts.insert(e.id.clone(), chart_data);
}
TemplateElement::Line(_) => {}
TemplateElement::Shape(_) => {}
TemplateElement::PageBreak(_) => {}
}
}
fn resolve_chart_data(e: &ChartElement, items: &[Value]) -> ResolvedChartData {
let (categories, series) = if let Some(ref group_field) = e.group_field {
// Grouped: her distinct group değeri bir seri olur
let mut category_order: Vec<String> = Vec::new();
let mut category_set = std::collections::HashSet::new();
let mut group_order: Vec<String> = Vec::new();
let mut group_set = std::collections::HashSet::new();
// group_name → (category → value) (birden fazla aynı group+category olursa topla)
let mut group_data: HashMap<String, HashMap<String, f64>> = HashMap::new();
for item in items {
let cat = value_to_string(resolve_path(item, &e.category_field));
let val = resolve_path(item, &e.value_field).as_f64().unwrap_or(0.0);
let grp = value_to_string(resolve_path(item, group_field));
if category_set.insert(cat.clone()) {
category_order.push(cat.clone());
}
if group_set.insert(grp.clone()) {
group_order.push(grp.clone());
}
*group_data.entry(grp).or_default().entry(cat).or_insert(0.0) += val;
}
let series = group_order
.iter()
.map(|grp| {
let grp_map = group_data.get(grp).unwrap();
let values = category_order
.iter()
.map(|cat| *grp_map.get(cat).unwrap_or(&0.0))
.collect();
ChartSeries {
name: grp.clone(),
values,
}
})
.collect();
(category_order, series)
} else {
// Tek seri
let mut categories = Vec::new();
let mut values = Vec::new();
for item in items {
categories.push(value_to_string(resolve_path(item, &e.category_field)));
values.push(resolve_path(item, &e.value_field).as_f64().unwrap_or(0.0));
}
let series = vec![ChartSeries {
name: e.value_field.clone(),
values,
}];
(categories, series)
};
ResolvedChartData {
chart_type: e.chart_type.clone(),
categories,
series,
title: e.title.clone(),
legend: e.legend.clone(),
labels: e.labels.clone(),
axis: e.axis.clone(),
style: e.style.clone(),
group_mode: e.group_mode.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_path_simple() {
let data: Value = serde_json::json!({"name": "test"});
assert_eq!(value_to_string(resolve_path(&data, "name")), "test");
}
#[test]
fn test_resolve_path_nested() {
let data: Value = serde_json::json!({
"firma": {
"unvan": "Acme A.Ş.",
"vergiNo": "123"
}
});
assert_eq!(
value_to_string(resolve_path(&data, "firma.unvan")),
"Acme A.Ş."
);
assert_eq!(value_to_string(resolve_path(&data, "firma.vergiNo")), "123");
}
#[test]
fn test_resolve_path_missing() {
let data: Value = serde_json::json!({"name": "test"});
let result = resolve_path(&data, "nonexistent.path");
assert!(result.is_null());
assert_eq!(value_to_string(result), "");
}
#[test]
fn test_resolve_path_deep_missing() {
let data: Value = serde_json::json!({"a": {"b": 42}});
let result = resolve_path(&data, "a.b.c.d");
assert!(result.is_null());
}
#[test]
fn test_value_to_string_types() {
assert_eq!(value_to_string(&serde_json::json!("hello")), "hello");
assert_eq!(value_to_string(&serde_json::json!(42)), "42");
assert_eq!(value_to_string(&serde_json::json!(3.14)), "3.14");
assert_eq!(value_to_string(&serde_json::json!(true)), "true");
assert_eq!(value_to_string(&serde_json::json!(null)), "");
}
#[test]
fn test_resolve_array() {
let data: Value = serde_json::json!({
"kalemler": [
{ "adi": "Widget", "tutar": 100 },
{ "adi": "Gadget", "tutar": 200 }
]
});
let arr = resolve_path(&data, "kalemler");
assert!(arr.is_array());
assert_eq!(arr.as_array().unwrap().len(), 2);
}
#[test]
fn test_resolve_template_text_binding() {
let template = Template {
id: "t1".to_string(),
name: "Test".to_string(),
page: PageSettings {
width: 210.0,
height: 297.0,
},
fonts: vec![],
header: None,
footer: None,
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
direction: "column".to_string(),
gap: 0.0,
padding: Padding::default(),
align: "stretch".to_string(),
justify: "start".to_string(),
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(),
style: TextStyle::default(),
content: None,
binding: ScalarBinding {
path: "firma.unvan".to_string(),
},
})],
},
};
let data = serde_json::json!({
"firma": { "unvan": "Acme Teknoloji A.Ş." }
});
let resolved = resolve_template(&template, &data);
assert_eq!(
resolved.texts.get("el_name").unwrap(),
"Acme Teknoloji A.Ş."
);
}
#[test]
fn test_resolve_template_text_with_prefix() {
let template = Template {
id: "t1".to_string(),
name: "Test".to_string(),
page: PageSettings {
width: 210.0,
height: 297.0,
},
fonts: vec![],
header: None,
footer: None,
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
direction: "column".to_string(),
gap: 0.0,
padding: Padding::default(),
align: "stretch".to_string(),
justify: "start".to_string(),
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(),
style: TextStyle::default(),
content: Some("Fatura No: ".to_string()),
binding: ScalarBinding {
path: "fatura.no".to_string(),
},
})],
},
};
let data = serde_json::json!({
"fatura": { "no": "FTR-001" }
});
let resolved = resolve_template(&template, &data);
assert_eq!(resolved.texts.get("el_no").unwrap(), "Fatura No: FTR-001");
}
#[test]
fn test_resolve_template_static_text() {
let template = Template {
id: "t1".to_string(),
name: "Test".to_string(),
page: PageSettings {
width: 210.0,
height: 297.0,
},
fonts: vec![],
header: None,
footer: None,
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
direction: "column".to_string(),
gap: 0.0,
padding: Padding::default(),
align: "stretch".to_string(),
justify: "start".to_string(),
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(),
style: TextStyle::default(),
content: "FATURA".to_string(),
})],
},
};
let resolved = resolve_template(&template, &serde_json::json!({}));
assert_eq!(resolved.texts.get("title").unwrap(), "FATURA");
}
#[test]
fn test_resolve_template_table_binding() {
let template = Template {
id: "t1".to_string(),
name: "Test".to_string(),
page: PageSettings {
width: 210.0,
height: 297.0,
},
fonts: vec![],
header: None,
footer: None,
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
direction: "column".to_string(),
gap: 0.0,
padding: Padding::default(),
align: "stretch".to_string(),
justify: "start".to_string(),
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(),
data_source: ArrayBinding {
path: "kalemler".to_string(),
},
columns: vec![
TableColumn {
id: "col_adi".to_string(),
field: "adi".to_string(),
title: "Urun Adi".to_string(),
width: SizeValue::Fr { value: 1.0 },
align: "left".to_string(),
format: None,
},
TableColumn {
id: "col_tutar".to_string(),
field: "tutar".to_string(),
title: "Tutar".to_string(),
width: SizeValue::Fixed { value: 30.0 },
align: "right".to_string(),
format: None,
},
],
style: TableStyle::default(),
repeat_header: Some(true),
})],
},
};
let data = serde_json::json!({
"kalemler": [
{ "adi": "Widget", "tutar": 100 },
{ "adi": "Gadget", "tutar": 200 }
]
});
let resolved = resolve_template(&template, &data);
let table = resolved.tables.get("tbl").unwrap();
assert_eq!(table.rows.len(), 2);
assert_eq!(table.rows[0], vec!["Widget", "100"]);
assert_eq!(table.rows[1], vec!["Gadget", "200"]);
}
#[test]
fn test_resolve_template_table_empty_array() {
let template = Template {
id: "t1".to_string(),
name: "Test".to_string(),
page: PageSettings {
width: 210.0,
height: 297.0,
},
fonts: vec![],
header: None,
footer: None,
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
direction: "column".to_string(),
gap: 0.0,
padding: Padding::default(),
align: "stretch".to_string(),
justify: "start".to_string(),
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(),
data_source: ArrayBinding {
path: "items".to_string(),
},
columns: vec![TableColumn {
id: "c1".to_string(),
field: "name".to_string(),
title: "Name".to_string(),
width: SizeValue::Fr { value: 1.0 },
align: "left".to_string(),
format: None,
}],
style: TableStyle::default(),
repeat_header: Some(true),
})],
},
};
let data = serde_json::json!({ "items": [] });
let resolved = resolve_template(&template, &data);
let table = resolved.tables.get("tbl").unwrap();
assert_eq!(table.rows.len(), 0);
}
#[test]
fn test_resolve_template_missing_binding_path() {
let template = Template {
id: "t1".to_string(),
name: "Test".to_string(),
page: PageSettings {
width: 210.0,
height: 297.0,
},
fonts: vec![],
header: None,
footer: None,
format_config: None,
locale: None,
root: ContainerElement {
id: "root".to_string(),
condition: None,
position: PositionMode::Flow,
size: SizeConstraint::default(),
direction: "column".to_string(),
gap: 0.0,
padding: Padding::default(),
align: "stretch".to_string(),
justify: "start".to_string(),
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(),
style: TextStyle::default(),
content: None,
binding: ScalarBinding {
path: "does.not.exist".to_string(),
},
})],
},
};
let data = serde_json::json!({});
let resolved = resolve_template(&template, &data);
// Missing binding path should resolve to empty string
assert_eq!(resolved.texts.get("el_missing").unwrap(), "");
}
}