mirror of
https://github.com/duhanbalci/dreport.git
synced 2026-07-02 02:49:16 +00:00
add elements
This commit is contained in:
995
layout-engine/src/page_break.rs
Normal file
995
layout-engine/src/page_break.rs
Normal file
@@ -0,0 +1,995 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::{ElementLayout, PageLayout, ResolvedContent};
|
||||
|
||||
/// Sayfa bölme girdi yapısı
|
||||
pub struct PageSplitInput {
|
||||
/// Body elemanları (sınırsız yükseklikte hesaplanmış, mutlak mm koordinatları)
|
||||
pub body_elements: Vec<ElementLayout>,
|
||||
/// Sayfa yüksekliği (mm)
|
||||
pub page_height_mm: f64,
|
||||
/// Header yüksekliği (mm) — body'nin başlangıç offset'i
|
||||
pub header_height_mm: f64,
|
||||
/// Footer yüksekliği (mm)
|
||||
pub footer_height_mm: f64,
|
||||
/// Header elemanları (klonlanacak, her sayfada tekrar)
|
||||
pub header_elements: Vec<ElementLayout>,
|
||||
/// Footer elemanları (klonlanacak, her sayfada tekrar)
|
||||
pub footer_elements: Vec<ElementLayout>,
|
||||
/// Sayfa genişliği (mm)
|
||||
pub page_width_mm: f64,
|
||||
/// Container break modları: element_id → "auto" | "avoid"
|
||||
pub break_modes: HashMap<String, String>,
|
||||
/// page_number format string'leri: element_id → format
|
||||
pub page_number_formats: HashMap<String, String>,
|
||||
/// Root container'ın üst padding'i (mm) — sayfa 2+ için body offset
|
||||
pub root_padding_top_mm: f64,
|
||||
}
|
||||
|
||||
/// Body elemanlarını sayfalara böl, header/footer ekle, page number'ları çöz.
|
||||
pub fn split_into_pages(input: PageSplitInput) -> Vec<PageLayout> {
|
||||
let content_height = input.page_height_mm - input.header_height_mm - input.footer_height_mm;
|
||||
|
||||
if content_height <= 0.0 {
|
||||
// Header + footer sayfaya sığmıyor — tek sayfa döndür
|
||||
return vec![assemble_page(
|
||||
0,
|
||||
&input.body_elements,
|
||||
&input.header_elements,
|
||||
&input.footer_elements,
|
||||
input.page_width_mm,
|
||||
input.page_height_mm,
|
||||
input.header_height_mm,
|
||||
input.footer_height_mm,
|
||||
0.0,
|
||||
input.root_padding_top_mm,
|
||||
)];
|
||||
}
|
||||
|
||||
// Parent lookup: element_id → parent_id (children alanından)
|
||||
let parent_map = build_parent_map(&input.body_elements);
|
||||
|
||||
// "avoid" grupları: container_id → (top_mm, bottom_mm, tüm descendant id'leri)
|
||||
let avoid_groups = build_avoid_groups(&input.body_elements, &input.break_modes, &parent_map);
|
||||
|
||||
// Tablo yapısı tespiti: table_id → header element id'leri
|
||||
let table_info = detect_table_structure(&input.body_elements);
|
||||
|
||||
// Elemanları sayfalara böl
|
||||
let page_slices = split_elements(
|
||||
&input.body_elements,
|
||||
content_height,
|
||||
&avoid_groups,
|
||||
&parent_map,
|
||||
&table_info,
|
||||
);
|
||||
|
||||
let total_pages = page_slices.len().max(1);
|
||||
|
||||
let mut pages: Vec<PageLayout> = Vec::with_capacity(total_pages);
|
||||
|
||||
for (page_idx, slice) in page_slices.iter().enumerate() {
|
||||
let page = assemble_page(
|
||||
page_idx,
|
||||
&slice.elements,
|
||||
&input.header_elements,
|
||||
&input.footer_elements,
|
||||
input.page_width_mm,
|
||||
input.page_height_mm,
|
||||
input.header_height_mm,
|
||||
input.footer_height_mm,
|
||||
slice.y_offset,
|
||||
input.root_padding_top_mm,
|
||||
);
|
||||
pages.push(page);
|
||||
}
|
||||
|
||||
// Boş sayfa koruması
|
||||
if pages.is_empty() {
|
||||
pages.push(assemble_page(
|
||||
0,
|
||||
&[],
|
||||
&input.header_elements,
|
||||
&input.footer_elements,
|
||||
input.page_width_mm,
|
||||
input.page_height_mm,
|
||||
input.header_height_mm,
|
||||
input.footer_height_mm,
|
||||
0.0,
|
||||
input.root_padding_top_mm,
|
||||
));
|
||||
}
|
||||
|
||||
// Page number çözümleme
|
||||
let total = pages.len();
|
||||
for (page_idx, page) in pages.iter_mut().enumerate() {
|
||||
resolve_page_numbers(&mut page.elements, page_idx + 1, total, &input.page_number_formats);
|
||||
}
|
||||
|
||||
pages
|
||||
}
|
||||
|
||||
/// Bir avoid grubunun bilgisi
|
||||
struct AvoidGroup {
|
||||
top_mm: f64,
|
||||
bottom_mm: f64,
|
||||
element_ids: HashSet<String>,
|
||||
}
|
||||
|
||||
/// Tablo yapısı bilgisi
|
||||
struct TableInfo {
|
||||
/// table_id → header satırının eleman id'leri
|
||||
_header_element_ids: Vec<String>,
|
||||
/// table_id → header satırındaki elemanların klonları
|
||||
header_elements: Vec<ElementLayout>,
|
||||
/// Header yüksekliği (mm)
|
||||
header_height_mm: f64,
|
||||
}
|
||||
|
||||
/// Sayfa dilimi
|
||||
struct PageSlice {
|
||||
elements: Vec<ElementLayout>,
|
||||
y_offset: f64, // Bu sayfanın strip'teki başlangıç y koordinatı
|
||||
}
|
||||
|
||||
fn build_parent_map(elements: &[ElementLayout]) -> HashMap<String, String> {
|
||||
let mut map = HashMap::new();
|
||||
for el in elements {
|
||||
for child_id in &el.children {
|
||||
map.insert(child_id.clone(), el.id.clone());
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
fn build_avoid_groups(
|
||||
elements: &[ElementLayout],
|
||||
break_modes: &HashMap<String, String>,
|
||||
_parent_map: &HashMap<String, String>,
|
||||
) -> Vec<AvoidGroup> {
|
||||
// Hangi container'lar avoid?
|
||||
let avoid_ids: HashSet<&String> = break_modes
|
||||
.iter()
|
||||
.filter(|(_, mode)| mode.as_str() == "avoid")
|
||||
.map(|(id, _)| id)
|
||||
.collect();
|
||||
|
||||
if avoid_ids.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let element_map: HashMap<&str, &ElementLayout> =
|
||||
elements.iter().map(|e| (e.id.as_str(), e)).collect();
|
||||
|
||||
let mut groups = Vec::new();
|
||||
|
||||
for avoid_id in &avoid_ids {
|
||||
if let Some(container) = element_map.get(avoid_id.as_str()) {
|
||||
// Bu container'ın tüm descendant'larını bul
|
||||
let mut descendant_ids = HashSet::new();
|
||||
descendant_ids.insert(container.id.clone());
|
||||
collect_descendants(container.id.as_str(), elements, &mut descendant_ids);
|
||||
|
||||
// Grubun top/bottom'ını hesapla
|
||||
let mut top = container.y_mm;
|
||||
let mut bottom = container.y_mm + container.height_mm;
|
||||
for el in elements {
|
||||
if descendant_ids.contains(&el.id) {
|
||||
top = top.min(el.y_mm);
|
||||
bottom = bottom.max(el.y_mm + el.height_mm);
|
||||
}
|
||||
}
|
||||
|
||||
groups.push(AvoidGroup {
|
||||
top_mm: top,
|
||||
bottom_mm: bottom,
|
||||
element_ids: descendant_ids,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
groups
|
||||
}
|
||||
|
||||
fn collect_descendants(
|
||||
parent_id: &str,
|
||||
elements: &[ElementLayout],
|
||||
result: &mut HashSet<String>,
|
||||
) {
|
||||
// children alanından recursive olarak topla
|
||||
for el in elements {
|
||||
if el.id == parent_id {
|
||||
for child_id in &el.children {
|
||||
result.insert(child_id.clone());
|
||||
collect_descendants(child_id, elements, result);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bir elemanın en yakın avoid ancestor'ı var mı?
|
||||
fn find_avoid_group<'a>(
|
||||
element_id: &str,
|
||||
avoid_groups: &'a [AvoidGroup],
|
||||
) -> Option<&'a AvoidGroup> {
|
||||
avoid_groups
|
||||
.iter()
|
||||
.find(|g| g.element_ids.contains(element_id))
|
||||
}
|
||||
|
||||
fn detect_table_structure(elements: &[ElementLayout]) -> HashMap<String, TableInfo> {
|
||||
// Tablo yapısını ID pattern'inden tespit et:
|
||||
// {table_id}_header → header satırı container
|
||||
// {table_id}_hdr_{N} → header hücreleri
|
||||
// {table_id}_row_{N} → veri satırı container
|
||||
// {table_id}_r{N}c{M} → veri hücreleri
|
||||
|
||||
let mut tables: HashMap<String, TableInfo> = HashMap::new();
|
||||
|
||||
// Önce header container'ları bul
|
||||
for el in elements {
|
||||
if el.id.ends_with("_header") && el.element_type == "container" {
|
||||
let table_id = el.id.trim_end_matches("_header").to_string();
|
||||
// Bu table_id ile başlayan row'lar var mı kontrol et
|
||||
let has_rows = elements
|
||||
.iter()
|
||||
.any(|e| e.id.starts_with(&format!("{}_row_", table_id)));
|
||||
if has_rows {
|
||||
// Header elemanlarını topla (header container + children)
|
||||
let mut header_ids = vec![el.id.clone()];
|
||||
for child_id in &el.children {
|
||||
header_ids.push(child_id.clone());
|
||||
}
|
||||
|
||||
let header_elements: Vec<ElementLayout> = elements
|
||||
.iter()
|
||||
.filter(|e| header_ids.contains(&e.id))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
let header_height = el.height_mm;
|
||||
|
||||
tables.insert(
|
||||
table_id,
|
||||
TableInfo {
|
||||
_header_element_ids: header_ids,
|
||||
header_elements,
|
||||
header_height_mm: header_height,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tables
|
||||
}
|
||||
|
||||
/// Hangi tablo'ya ait bir satır elemanı mı?
|
||||
fn detect_table_row(element_id: &str) -> Option<(String, usize)> {
|
||||
// Pattern: {table_id}_row_{N}
|
||||
if let Some(pos) = element_id.rfind("_row_") {
|
||||
let table_id = element_id[..pos].to_string();
|
||||
let row_str = &element_id[pos + 5..];
|
||||
if let Ok(row_idx) = row_str.parse::<usize>() {
|
||||
return Some((table_id, row_idx));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn split_elements(
|
||||
elements: &[ElementLayout],
|
||||
content_height: f64,
|
||||
avoid_groups: &[AvoidGroup],
|
||||
_parent_map: &HashMap<String, String>,
|
||||
table_info: &HashMap<String, TableInfo>,
|
||||
) -> Vec<PageSlice> {
|
||||
if elements.is_empty() {
|
||||
return vec![PageSlice {
|
||||
elements: vec![],
|
||||
y_offset: 0.0,
|
||||
}];
|
||||
}
|
||||
|
||||
let mut pages: Vec<PageSlice> = vec![PageSlice {
|
||||
elements: Vec::new(),
|
||||
y_offset: 0.0,
|
||||
}];
|
||||
|
||||
// Yapısal container'ları tespit et: çocukları arasında container olan container'lar.
|
||||
// Bu container'lar sayfa sınırında bölünebilir (çocukları bireysel sayfa bölmesi yapar).
|
||||
// Aksine, çocukları sadece leaf olan container'lar (ör. tablo satırı) atomik kalır.
|
||||
let element_type_map: HashMap<&str, &str> = elements
|
||||
.iter()
|
||||
.map(|e| (e.id.as_str(), e.element_type.as_str()))
|
||||
.collect();
|
||||
let splittable_containers: HashSet<&str> = elements
|
||||
.iter()
|
||||
.filter(|e| e.element_type == "container")
|
||||
.filter(|e| {
|
||||
e.children
|
||||
.iter()
|
||||
.any(|child_id| element_type_map.get(child_id.as_str()) == Some(&"container"))
|
||||
})
|
||||
.map(|e| e.id.as_str())
|
||||
.collect();
|
||||
|
||||
let mut page_top = 0.0; // Mevcut sayfanın strip'teki başlangıç y'si
|
||||
let mut processed: HashSet<String> = HashSet::new();
|
||||
// Hangi tablo'ların header'ı bu sayfada zaten var?
|
||||
let mut table_header_on_page: HashSet<String> = HashSet::new();
|
||||
|
||||
for el in elements {
|
||||
if processed.contains(&el.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// page_break elemanı → mevcut sayfaya ekle, sonra yeni sayfa zorla
|
||||
if el.element_type == "page_break" {
|
||||
pages.last_mut().unwrap().elements.push(el.clone());
|
||||
processed.insert(el.id.clone());
|
||||
page_top = el.y_mm + el.height_mm;
|
||||
pages.push(PageSlice {
|
||||
elements: Vec::new(),
|
||||
y_offset: page_top,
|
||||
});
|
||||
table_header_on_page.clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
let el_top = el.y_mm;
|
||||
let el_bottom = el.y_mm + el.height_mm;
|
||||
let relative_bottom = el_bottom - page_top;
|
||||
|
||||
// Avoid group kontrolü
|
||||
if let Some(group) = find_avoid_group(&el.id, avoid_groups) {
|
||||
let group_relative_bottom = group.bottom_mm - page_top;
|
||||
let group_height = group.bottom_mm - group.top_mm;
|
||||
|
||||
if group_relative_bottom > content_height && group_height <= content_height {
|
||||
// Grup mevcut sayfaya sığmıyor ama tek sayfaya sığar → yeni sayfa
|
||||
page_top = group.top_mm;
|
||||
pages.push(PageSlice {
|
||||
elements: Vec::new(),
|
||||
y_offset: page_top,
|
||||
});
|
||||
table_header_on_page.clear();
|
||||
}
|
||||
// Grup sayfadan büyükse → normal akışa devam (bölünemez ama mecbur)
|
||||
}
|
||||
|
||||
// Eleman mevcut sayfaya sığıyor mu?
|
||||
if relative_bottom > content_height && el_top > page_top {
|
||||
// Yapısal container (çocukları container olan) → bölünebilir.
|
||||
// Komple yeni sayfaya atmak yerine mevcut sayfada bırak,
|
||||
// çocuk elemanlar bireysel olarak sayfa bölmesini halledecek.
|
||||
if splittable_containers.contains(el.id.as_str()) {
|
||||
pages.last_mut().unwrap().elements.push(el.clone());
|
||||
processed.insert(el.id.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sığmıyor → yeni sayfa
|
||||
|
||||
// Tablo satırı mı? Header tekrarı gerekebilir
|
||||
let mut table_header_to_add: Option<(String, Vec<ElementLayout>, f64)> = None;
|
||||
if let Some((table_id, _row_idx)) = detect_table_row(&el.id) {
|
||||
if let Some(info) = table_info.get(&table_id) {
|
||||
// Yeni sayfada bu tablonun header'ını tekrarla
|
||||
table_header_to_add =
|
||||
Some((table_id.clone(), info.header_elements.clone(), info.header_height_mm));
|
||||
}
|
||||
}
|
||||
|
||||
page_top = el_top;
|
||||
|
||||
// Tablo header tekrarı varsa, header yüksekliği kadar offset
|
||||
if let Some((ref table_id, ref header_els, header_h)) = table_header_to_add {
|
||||
// Header'ı yeni sayfanın başına koy (offset'li)
|
||||
page_top = el_top - header_h;
|
||||
let new_page_idx = pages.len();
|
||||
pages.push(PageSlice {
|
||||
elements: Vec::new(),
|
||||
y_offset: page_top,
|
||||
});
|
||||
table_header_on_page.clear();
|
||||
|
||||
// Header elemanlarını klonla ve y pozisyonlarını yeni sayfaya taşı.
|
||||
// Orijinal header elemanları tablonun ilk konumundaki y değerlerine sahip.
|
||||
// Yeni sayfada page_top'tan başlamaları gerekir.
|
||||
let orig_header_y = header_els
|
||||
.iter()
|
||||
.map(|e| e.y_mm)
|
||||
.min_by(|a, b| a.partial_cmp(b).unwrap())
|
||||
.unwrap_or(page_top);
|
||||
let y_shift = page_top - orig_header_y;
|
||||
|
||||
for hdr_el in header_els {
|
||||
let mut cloned = hdr_el.clone();
|
||||
cloned.y_mm += y_shift;
|
||||
cloned.id = format!("{}_p{}", hdr_el.id, new_page_idx);
|
||||
pages.last_mut().unwrap().elements.push(cloned);
|
||||
}
|
||||
table_header_on_page.insert(table_id.clone());
|
||||
} else {
|
||||
pages.push(PageSlice {
|
||||
elements: Vec::new(),
|
||||
y_offset: page_top,
|
||||
});
|
||||
table_header_on_page.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Elemanı mevcut sayfaya ekle
|
||||
pages.last_mut().unwrap().elements.push(el.clone());
|
||||
processed.insert(el.id.clone());
|
||||
}
|
||||
|
||||
pages
|
||||
}
|
||||
|
||||
fn assemble_page(
|
||||
page_index: usize,
|
||||
body_elements: &[ElementLayout],
|
||||
header_elements: &[ElementLayout],
|
||||
footer_elements: &[ElementLayout],
|
||||
page_width_mm: f64,
|
||||
page_height_mm: f64,
|
||||
header_height_mm: f64,
|
||||
footer_height_mm: f64,
|
||||
body_y_offset: f64,
|
||||
root_padding_top_mm: f64,
|
||||
) -> PageLayout {
|
||||
let mut elements = Vec::new();
|
||||
|
||||
// Header elemanları (y = orijinal y, sayfa başında)
|
||||
for el in header_elements {
|
||||
let mut cloned = el.clone();
|
||||
if page_index > 0 {
|
||||
// Sonraki sayfalarda ID'yi unique yap
|
||||
cloned.id = format!("{}_p{}", el.id, page_index);
|
||||
}
|
||||
elements.push(cloned);
|
||||
}
|
||||
|
||||
// Body elemanları (y offset'li — strip y'den sayfa-relative y'ye)
|
||||
// Sayfa 2+ için root padding tekrar eklenir (root container sadece sayfa 1'de var)
|
||||
let extra_top = if page_index > 0 { root_padding_top_mm } else { 0.0 };
|
||||
for el in body_elements {
|
||||
let mut adjusted = el.clone();
|
||||
adjusted.y_mm = el.y_mm - body_y_offset + header_height_mm + extra_top;
|
||||
elements.push(adjusted);
|
||||
}
|
||||
|
||||
// Footer elemanları (sayfanın altında)
|
||||
let footer_y_offset = page_height_mm - footer_height_mm;
|
||||
for el in footer_elements {
|
||||
let mut cloned = el.clone();
|
||||
// Footer elemanlarının y'si footer container'ın başlangıcına relative
|
||||
// Footer'ın orijinal y'si 0'dan başlıyor (ayrı hesaplanıyor)
|
||||
// Sayfa içi pozisyon: footer_y_offset + orijinal y
|
||||
cloned.y_mm = el.y_mm + footer_y_offset;
|
||||
if page_index > 0 {
|
||||
cloned.id = format!("{}_p{}", el.id, page_index);
|
||||
}
|
||||
elements.push(cloned);
|
||||
}
|
||||
|
||||
PageLayout {
|
||||
page_index,
|
||||
width_mm: page_width_mm,
|
||||
height_mm: page_height_mm,
|
||||
elements,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_page_numbers(
|
||||
elements: &mut [ElementLayout],
|
||||
current_page: usize,
|
||||
total_pages: usize,
|
||||
formats: &HashMap<String, String>,
|
||||
) {
|
||||
for el in elements.iter_mut() {
|
||||
if el.element_type != "page_number" {
|
||||
continue;
|
||||
}
|
||||
|
||||
// ID'den orijinal format ID'sini çıkar (sayfa klonları _p{N} ile biter)
|
||||
let original_id = if let Some(pos) = el.id.rfind("_p") {
|
||||
let suffix = &el.id[pos + 2..];
|
||||
if suffix.parse::<usize>().is_ok() {
|
||||
&el.id[..pos]
|
||||
} else {
|
||||
&el.id
|
||||
}
|
||||
} else {
|
||||
&el.id
|
||||
};
|
||||
|
||||
let fmt = formats
|
||||
.get(original_id)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("{current} / {total}");
|
||||
|
||||
let text = fmt
|
||||
.replace("{current}", ¤t_page.to_string())
|
||||
.replace("{total}", &total_pages.to_string());
|
||||
|
||||
el.content = Some(ResolvedContent::PageNumber {
|
||||
current: current_page,
|
||||
total: total_pages,
|
||||
});
|
||||
// Ayrıca text content'i de güncelle (LayoutRenderer text olarak render ediyor)
|
||||
// PageNumber render'da content.type === "text" kontrolü var, text olarak da ekle
|
||||
el.content = Some(ResolvedContent::Text { value: text });
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ResolvedStyle;
|
||||
|
||||
fn make_element(id: &str, y: f64, height: f64, element_type: &str) -> ElementLayout {
|
||||
ElementLayout {
|
||||
id: id.to_string(),
|
||||
x_mm: 0.0,
|
||||
y_mm: y,
|
||||
width_mm: 180.0,
|
||||
height_mm: height,
|
||||
element_type: element_type.to_string(),
|
||||
content: None,
|
||||
style: ResolvedStyle::default(),
|
||||
children: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_page_no_split() {
|
||||
let input = PageSplitInput {
|
||||
body_elements: vec![
|
||||
make_element("el1", 0.0, 50.0, "text"),
|
||||
make_element("el2", 50.0, 50.0, "text"),
|
||||
],
|
||||
page_height_mm: 297.0,
|
||||
header_height_mm: 0.0,
|
||||
footer_height_mm: 0.0,
|
||||
header_elements: vec![],
|
||||
footer_elements: vec![],
|
||||
page_width_mm: 210.0,
|
||||
break_modes: HashMap::new(),
|
||||
page_number_formats: HashMap::new(),
|
||||
root_padding_top_mm: 0.0,
|
||||
};
|
||||
|
||||
let pages = split_into_pages(input);
|
||||
assert_eq!(pages.len(), 1);
|
||||
assert_eq!(pages[0].elements.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auto_page_break() {
|
||||
let input = PageSplitInput {
|
||||
body_elements: vec![
|
||||
make_element("el1", 0.0, 200.0, "text"),
|
||||
make_element("el2", 200.0, 200.0, "text"),
|
||||
],
|
||||
page_height_mm: 297.0,
|
||||
header_height_mm: 0.0,
|
||||
footer_height_mm: 0.0,
|
||||
header_elements: vec![],
|
||||
footer_elements: vec![],
|
||||
page_width_mm: 210.0,
|
||||
break_modes: HashMap::new(),
|
||||
page_number_formats: HashMap::new(),
|
||||
root_padding_top_mm: 0.0,
|
||||
};
|
||||
|
||||
let pages = split_into_pages(input);
|
||||
assert_eq!(pages.len(), 2);
|
||||
assert_eq!(pages[0].elements.len(), 1);
|
||||
assert_eq!(pages[0].elements[0].id, "el1");
|
||||
assert_eq!(pages[1].elements.len(), 1);
|
||||
assert_eq!(pages[1].elements[0].id, "el2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manual_page_break() {
|
||||
let input = PageSplitInput {
|
||||
body_elements: vec![
|
||||
make_element("el1", 0.0, 50.0, "text"),
|
||||
make_element("pb1", 50.0, 0.0, "page_break"),
|
||||
make_element("el2", 50.0, 50.0, "text"),
|
||||
],
|
||||
page_height_mm: 297.0,
|
||||
header_height_mm: 0.0,
|
||||
footer_height_mm: 0.0,
|
||||
header_elements: vec![],
|
||||
footer_elements: vec![],
|
||||
page_width_mm: 210.0,
|
||||
break_modes: HashMap::new(),
|
||||
page_number_formats: HashMap::new(),
|
||||
root_padding_top_mm: 0.0,
|
||||
};
|
||||
|
||||
let pages = split_into_pages(input);
|
||||
assert_eq!(pages.len(), 2);
|
||||
assert_eq!(pages[0].elements.len(), 2); // el1 + pb1
|
||||
assert_eq!(pages[1].elements.len(), 1); // el2
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_header_footer_on_all_pages() {
|
||||
let header = vec![make_element("hdr", 0.0, 15.0, "text")];
|
||||
let footer = vec![make_element("ftr", 0.0, 10.0, "text")];
|
||||
|
||||
let input = PageSplitInput {
|
||||
body_elements: vec![
|
||||
make_element("el1", 0.0, 200.0, "text"),
|
||||
make_element("el2", 200.0, 200.0, "text"),
|
||||
],
|
||||
page_height_mm: 297.0,
|
||||
header_height_mm: 15.0,
|
||||
footer_height_mm: 10.0,
|
||||
header_elements: header,
|
||||
footer_elements: footer,
|
||||
page_width_mm: 210.0,
|
||||
break_modes: HashMap::new(),
|
||||
page_number_formats: HashMap::new(),
|
||||
root_padding_top_mm: 0.0,
|
||||
};
|
||||
|
||||
let pages = split_into_pages(input);
|
||||
assert_eq!(pages.len(), 2);
|
||||
// Her sayfada header + body + footer var
|
||||
// Sayfa 1: hdr + el1 + ftr = 3
|
||||
assert!(pages[0].elements.iter().any(|e| e.id == "hdr"));
|
||||
assert!(pages[0].elements.iter().any(|e| e.id == "el1"));
|
||||
assert!(pages[0].elements.iter().any(|e| e.id == "ftr"));
|
||||
// Sayfa 2: hdr_p1 + el2 + ftr_p1 = 3
|
||||
assert!(pages[1].elements.iter().any(|e| e.id == "hdr_p1"));
|
||||
assert!(pages[1].elements.iter().any(|e| e.id == "el2"));
|
||||
assert!(pages[1].elements.iter().any(|e| e.id == "ftr_p1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_page_numbers_resolved() {
|
||||
let mut formats = HashMap::new();
|
||||
formats.insert("pn".to_string(), "{current} / {total}".to_string());
|
||||
|
||||
let input = PageSplitInput {
|
||||
body_elements: vec![
|
||||
make_element("el1", 0.0, 200.0, "text"),
|
||||
make_element("el2", 200.0, 200.0, "text"),
|
||||
],
|
||||
page_height_mm: 297.0,
|
||||
header_height_mm: 15.0,
|
||||
footer_height_mm: 10.0,
|
||||
header_elements: vec![{
|
||||
let mut el = make_element("pn", 0.0, 10.0, "page_number");
|
||||
el.content = Some(ResolvedContent::Text {
|
||||
value: "1 / 1".to_string(),
|
||||
});
|
||||
el
|
||||
}],
|
||||
footer_elements: vec![],
|
||||
page_width_mm: 210.0,
|
||||
break_modes: HashMap::new(),
|
||||
page_number_formats: formats,
|
||||
root_padding_top_mm: 0.0,
|
||||
};
|
||||
|
||||
let pages = split_into_pages(input);
|
||||
assert_eq!(pages.len(), 2);
|
||||
|
||||
// Sayfa 1: pn → "1 / 2"
|
||||
let pn1 = pages[0].elements.iter().find(|e| e.id == "pn").unwrap();
|
||||
if let Some(ResolvedContent::Text { value }) = &pn1.content {
|
||||
assert_eq!(value, "1 / 2");
|
||||
} else {
|
||||
panic!("page_number content should be text");
|
||||
}
|
||||
|
||||
// Sayfa 2: pn_p1 → "2 / 2"
|
||||
let pn2 = pages[1]
|
||||
.elements
|
||||
.iter()
|
||||
.find(|e| e.id == "pn_p1")
|
||||
.unwrap();
|
||||
if let Some(ResolvedContent::Text { value }) = &pn2.content {
|
||||
assert_eq!(value, "2 / 2");
|
||||
} else {
|
||||
panic!("page_number content should be text");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_splits_across_pages_not_jumps() {
|
||||
// Tablo wrapper container sayfa yüksekliğinden büyük olduğunda,
|
||||
// komple yeni sayfaya atlamak yerine satırları sayfalara bölmeli.
|
||||
//
|
||||
// Senaryo: sayfa 200mm, content 200mm (header/footer yok).
|
||||
// Tablonun öncesinde 50mm'lik bir eleman var.
|
||||
// Tablo wrapper: y=50, h=300 (sayfaya sığmaz).
|
||||
// Tablo satırları: her biri 30mm.
|
||||
// Beklenen: ilk sayfa = el1 + tbl wrapper + header + ilk ~5 satır,
|
||||
// ikinci sayfa = kalan satırlar.
|
||||
|
||||
let mut tbl_wrapper = make_element("tbl", 50.0, 300.0, "container");
|
||||
tbl_wrapper.children = vec![
|
||||
"tbl_header".to_string(),
|
||||
"tbl_row_0".to_string(),
|
||||
"tbl_row_1".to_string(),
|
||||
"tbl_row_2".to_string(),
|
||||
"tbl_row_3".to_string(),
|
||||
"tbl_row_4".to_string(),
|
||||
"tbl_row_5".to_string(),
|
||||
"tbl_row_6".to_string(),
|
||||
"tbl_row_7".to_string(),
|
||||
"tbl_row_8".to_string(),
|
||||
"tbl_row_9".to_string(),
|
||||
];
|
||||
|
||||
let tbl_header = {
|
||||
let mut el = make_element("tbl_header", 50.0, 20.0, "container");
|
||||
el.children = vec!["tbl_hdr_0".to_string()];
|
||||
el
|
||||
};
|
||||
let tbl_hdr_0 = make_element("tbl_hdr_0", 50.0, 20.0, "static_text");
|
||||
|
||||
// 10 satır, her biri 28mm (gap dahil), y=70'ten başlıyor
|
||||
let rows: Vec<ElementLayout> = (0..10)
|
||||
.flat_map(|i| {
|
||||
let y = 70.0 + (i as f64) * 28.0;
|
||||
let mut row = make_element(&format!("tbl_row_{}", i), y, 28.0, "container");
|
||||
row.children = vec![format!("tbl_r{}c0", i)];
|
||||
let cell = make_element(&format!("tbl_r{}c0", i), y, 28.0, "static_text");
|
||||
vec![row, cell]
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut body_elements = vec![
|
||||
make_element("el1", 0.0, 50.0, "text"),
|
||||
tbl_wrapper,
|
||||
tbl_header,
|
||||
tbl_hdr_0,
|
||||
];
|
||||
body_elements.extend(rows);
|
||||
|
||||
let input = PageSplitInput {
|
||||
body_elements,
|
||||
page_height_mm: 200.0,
|
||||
header_height_mm: 0.0,
|
||||
footer_height_mm: 0.0,
|
||||
header_elements: vec![],
|
||||
footer_elements: vec![],
|
||||
page_width_mm: 210.0,
|
||||
break_modes: HashMap::new(),
|
||||
page_number_formats: HashMap::new(),
|
||||
root_padding_top_mm: 0.0,
|
||||
};
|
||||
|
||||
let pages = split_into_pages(input);
|
||||
|
||||
// Tablo komple 2. sayfaya atlamamalı!
|
||||
// Sayfa 1'de el1 + tablo başlangıcı olmalı
|
||||
assert!(
|
||||
pages[0].elements.iter().any(|e| e.id == "el1"),
|
||||
"el1 should be on page 1"
|
||||
);
|
||||
assert!(
|
||||
pages[0].elements.iter().any(|e| e.id == "tbl"),
|
||||
"table wrapper should start on page 1 (not jump to page 2)"
|
||||
);
|
||||
assert!(
|
||||
pages[0].elements.iter().any(|e| e.id == "tbl_header"),
|
||||
"table header should be on page 1"
|
||||
);
|
||||
assert!(
|
||||
pages[0].elements.iter().any(|e| e.id == "tbl_row_0"),
|
||||
"first table row should be on page 1"
|
||||
);
|
||||
|
||||
// En az 2 sayfa olmalı (tablo bölünmeli)
|
||||
assert!(
|
||||
pages.len() >= 2,
|
||||
"table should split across at least 2 pages, got {}",
|
||||
pages.len()
|
||||
);
|
||||
|
||||
// Son satırlar sonraki sayfa(lar)da olmalı
|
||||
let last_row_id = "tbl_row_9";
|
||||
let last_row_page = pages
|
||||
.iter()
|
||||
.position(|p| p.elements.iter().any(|e| e.id == last_row_id))
|
||||
.expect("last row should exist somewhere");
|
||||
assert!(
|
||||
last_row_page > 0,
|
||||
"last table row should be on a later page"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_header_repeats_on_new_page() {
|
||||
// Tablo satırı yeni sayfaya geçtiğinde, header tekrar edilmeli.
|
||||
//
|
||||
// Senaryo: sayfa 150mm, tablo y=0'dan başlıyor.
|
||||
// Header: 20mm, her satır 30mm → 4 satır = 120mm + header 20mm = 140mm (1. sayfa)
|
||||
// 5. satır sığmaz → 2. sayfaya geçer, header tekrar olmalı.
|
||||
|
||||
let mut tbl_wrapper = make_element("tbl", 0.0, 200.0, "container");
|
||||
tbl_wrapper.children = vec![
|
||||
"tbl_header".to_string(),
|
||||
"tbl_row_0".to_string(),
|
||||
"tbl_row_1".to_string(),
|
||||
"tbl_row_2".to_string(),
|
||||
"tbl_row_3".to_string(),
|
||||
"tbl_row_4".to_string(),
|
||||
"tbl_row_5".to_string(),
|
||||
];
|
||||
|
||||
let tbl_header = {
|
||||
let mut el = make_element("tbl_header", 0.0, 20.0, "container");
|
||||
el.children = vec!["tbl_hdr_0".to_string()];
|
||||
el
|
||||
};
|
||||
let tbl_hdr_0 = make_element("tbl_hdr_0", 0.0, 20.0, "static_text");
|
||||
|
||||
let rows: Vec<ElementLayout> = (0..6)
|
||||
.flat_map(|i| {
|
||||
let y = 20.0 + (i as f64) * 30.0;
|
||||
let mut row = make_element(&format!("tbl_row_{}", i), y, 30.0, "container");
|
||||
row.children = vec![format!("tbl_r{}c0", i)];
|
||||
let cell = make_element(&format!("tbl_r{}c0", i), y, 30.0, "static_text");
|
||||
vec![row, cell]
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut body_elements = vec![tbl_wrapper, tbl_header, tbl_hdr_0];
|
||||
body_elements.extend(rows);
|
||||
|
||||
let input = PageSplitInput {
|
||||
body_elements,
|
||||
page_height_mm: 150.0,
|
||||
header_height_mm: 0.0,
|
||||
footer_height_mm: 0.0,
|
||||
header_elements: vec![],
|
||||
footer_elements: vec![],
|
||||
page_width_mm: 210.0,
|
||||
break_modes: HashMap::new(),
|
||||
page_number_formats: HashMap::new(),
|
||||
root_padding_top_mm: 0.0,
|
||||
};
|
||||
|
||||
let pages = split_into_pages(input);
|
||||
|
||||
assert!(pages.len() >= 2, "should split into at least 2 pages");
|
||||
|
||||
// Sayfa 2'de tablo header'ının tekrar edilmiş kopyası olmalı
|
||||
let page2_has_header = pages[1]
|
||||
.elements
|
||||
.iter()
|
||||
.any(|e| e.id.starts_with("tbl_header"));
|
||||
assert!(
|
||||
page2_has_header,
|
||||
"table header should be repeated on page 2. Page 2 elements: {:?}",
|
||||
pages[1].elements.iter().map(|e| &e.id).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_repeated_header_no_gap_with_rows() {
|
||||
// Tekrarlanan header ile ilk satır arasında boşluk olmamalı.
|
||||
// Header'ın y pozisyonu yeni sayfanın başlangıcına relocate edilmeli.
|
||||
//
|
||||
// Senaryo: tablo y=100'de başlıyor, header 10mm, satırlar 8mm.
|
||||
// Sayfa content_height=80mm.
|
||||
// Satırlar: y=110, 118, 126, ... → relative_bottom kontrolü.
|
||||
|
||||
let mut tbl_wrapper = make_element("tbl", 100.0, 120.0, "container");
|
||||
tbl_wrapper.children = vec![
|
||||
"tbl_header".to_string(),
|
||||
"tbl_row_0".to_string(),
|
||||
"tbl_row_1".to_string(),
|
||||
"tbl_row_2".to_string(),
|
||||
"tbl_row_3".to_string(),
|
||||
"tbl_row_4".to_string(),
|
||||
"tbl_row_5".to_string(),
|
||||
"tbl_row_6".to_string(),
|
||||
"tbl_row_7".to_string(),
|
||||
"tbl_row_8".to_string(),
|
||||
"tbl_row_9".to_string(),
|
||||
];
|
||||
|
||||
let tbl_header = {
|
||||
let mut el = make_element("tbl_header", 100.0, 10.0, "container");
|
||||
el.children = vec!["tbl_hdr_0".to_string()];
|
||||
el
|
||||
};
|
||||
let tbl_hdr_0 = make_element("tbl_hdr_0", 100.0, 10.0, "static_text");
|
||||
|
||||
let rows: Vec<ElementLayout> = (0..10)
|
||||
.flat_map(|i| {
|
||||
let y = 110.0 + (i as f64) * 12.0;
|
||||
let mut row = make_element(&format!("tbl_row_{}", i), y, 12.0, "container");
|
||||
row.children = vec![format!("tbl_r{}c0", i)];
|
||||
let cell = make_element(&format!("tbl_r{}c0", i), y, 12.0, "static_text");
|
||||
vec![row, cell]
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut body_elements = vec![
|
||||
make_element("el1", 0.0, 50.0, "text"), // 50mm metin
|
||||
make_element("el2", 50.0, 50.0, "text"), // 50mm metin (toplam 100mm)
|
||||
tbl_wrapper,
|
||||
tbl_header,
|
||||
tbl_hdr_0,
|
||||
];
|
||||
body_elements.extend(rows);
|
||||
|
||||
// content_height = 200 - 15 - 10 = 175
|
||||
let doc_header = vec![make_element("doc_hdr", 0.0, 15.0, "text")];
|
||||
let doc_footer = vec![make_element("doc_ftr", 0.0, 10.0, "text")];
|
||||
|
||||
let input = PageSplitInput {
|
||||
body_elements,
|
||||
page_height_mm: 200.0,
|
||||
header_height_mm: 15.0,
|
||||
footer_height_mm: 10.0,
|
||||
header_elements: doc_header,
|
||||
footer_elements: doc_footer,
|
||||
page_width_mm: 210.0,
|
||||
break_modes: HashMap::new(),
|
||||
page_number_formats: HashMap::new(),
|
||||
root_padding_top_mm: 5.0,
|
||||
};
|
||||
|
||||
let pages = split_into_pages(input);
|
||||
assert!(pages.len() >= 2, "should have at least 2 pages");
|
||||
|
||||
// Sayfa 2'deki elemanlar
|
||||
let page2 = &pages[1];
|
||||
|
||||
// Tekrarlanan header'ı bul
|
||||
let repeated_header = page2
|
||||
.elements
|
||||
.iter()
|
||||
.find(|e| e.id.starts_with("tbl_header") && e.id != "tbl_header")
|
||||
.expect("repeated table header should exist on page 2");
|
||||
|
||||
// Header'dan sonraki ilk satırı bul
|
||||
let first_row_on_page2 = page2
|
||||
.elements
|
||||
.iter()
|
||||
.find(|e| e.id.starts_with("tbl_row_"))
|
||||
.expect("at least one table row should be on page 2");
|
||||
|
||||
// Header'ın alt kenarı ile satırın üst kenarı arasında boşluk olmamalı (veya çok az)
|
||||
let header_bottom = repeated_header.y_mm + repeated_header.height_mm;
|
||||
let row_top = first_row_on_page2.y_mm;
|
||||
let gap = (row_top - header_bottom).abs();
|
||||
|
||||
assert!(
|
||||
gap < 1.0,
|
||||
"gap between repeated header (bottom={:.1}) and first row (top={:.1}) should be < 1mm, got {:.1}mm",
|
||||
header_bottom,
|
||||
row_top,
|
||||
gap
|
||||
);
|
||||
|
||||
// Header y değeri negatif olmamalı
|
||||
assert!(
|
||||
repeated_header.y_mm >= 0.0,
|
||||
"repeated header y should be non-negative, got {:.1}",
|
||||
repeated_header.y_mm
|
||||
);
|
||||
|
||||
// Header, document header'dan sonra gelmeli
|
||||
assert!(
|
||||
repeated_header.y_mm >= 15.0,
|
||||
"repeated header should be after doc header (15mm), got {:.1}",
|
||||
repeated_header.y_mm
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user