1use crate::model::{
2 AlignmentDescriptor, AlignmentPatch, BorderSideDescriptor, BorderSidePatch, BordersDescriptor,
3 BordersPatch, FillDescriptor, FillPatch, FontDescriptor, FontPatch, GradientFillDescriptor,
4 GradientFillPatch, GradientStopDescriptor, PatternFillDescriptor, PatternFillPatch,
5 StyleDescriptor, StylePatch,
6};
7use sha2::{Digest, Sha256};
8use std::collections::BTreeMap;
9use std::str::FromStr;
10use umya_spreadsheet::structs::{EnumTrait, HorizontalAlignmentValues, VerticalAlignmentValues};
11use umya_spreadsheet::{Border, Fill, Font, PatternValues, Style};
12
13pub fn descriptor_from_style(style: &Style) -> StyleDescriptor {
14 let font = style.get_font().and_then(descriptor_from_font);
15 let fill = style.get_fill().and_then(descriptor_from_fill);
16 let borders = style.get_borders().and_then(|borders| {
17 let left = descriptor_from_border_side(borders.get_left_border());
18 let right = descriptor_from_border_side(borders.get_right_border());
19 let top = descriptor_from_border_side(borders.get_top_border());
20 let bottom = descriptor_from_border_side(borders.get_bottom_border());
21 let diagonal = descriptor_from_border_side(borders.get_diagonal_border());
22 let vertical = descriptor_from_border_side(borders.get_vertical_border());
23 let horizontal = descriptor_from_border_side(borders.get_horizontal_border());
24
25 let diagonal_up = if *borders.get_diagonal_up() {
26 Some(true)
27 } else {
28 None
29 };
30 let diagonal_down = if *borders.get_diagonal_down() {
31 Some(true)
32 } else {
33 None
34 };
35
36 let descriptor = BordersDescriptor {
37 left,
38 right,
39 top,
40 bottom,
41 diagonal,
42 vertical,
43 horizontal,
44 diagonal_up,
45 diagonal_down,
46 };
47
48 if descriptor.is_empty() {
49 None
50 } else {
51 Some(descriptor)
52 }
53 });
54 let alignment = style.get_alignment().and_then(descriptor_from_alignment);
55 let number_format = style.get_number_format().and_then(|fmt| {
56 let code = fmt.get_format_code();
57 if code.eq_ignore_ascii_case("general") {
58 None
59 } else {
60 Some(code.to_string())
61 }
62 });
63
64 StyleDescriptor {
65 font,
66 fill,
67 borders,
68 alignment,
69 number_format,
70 }
71}
72
73pub fn stable_style_id(descriptor: &StyleDescriptor) -> String {
74 let bytes = serde_json::to_vec(descriptor).unwrap_or_default();
75 let mut hasher = Sha256::new();
76 hasher.update(bytes);
77 let digest = hasher.finalize();
78 let hex = format!("{digest:x}");
79 hex.chars().take(12).collect()
80}
81
82pub fn compress_positions_to_ranges(positions: &[(u32, u32)], limit: usize) -> (Vec<String>, bool) {
83 if positions.is_empty() {
84 return (Vec::new(), false);
85 }
86
87 let mut rows: BTreeMap<u32, Vec<u32>> = BTreeMap::new();
88 for &(row, col) in positions {
89 rows.entry(row).or_default().push(col);
90 }
91 for cols in rows.values_mut() {
92 cols.sort_unstable();
93 cols.dedup();
94 }
95
96 let mut spans_by_cols: BTreeMap<(u32, u32), Vec<u32>> = BTreeMap::new();
97 for (row, cols) in rows {
98 if cols.is_empty() {
99 continue;
100 }
101 let mut start = cols[0];
102 let mut prev = cols[0];
103 for col in cols.into_iter().skip(1) {
104 if col == prev + 1 {
105 prev = col;
106 } else {
107 spans_by_cols.entry((start, prev)).or_default().push(row);
108 start = col;
109 prev = col;
110 }
111 }
112 spans_by_cols.entry((start, prev)).or_default().push(row);
113 }
114
115 let mut ranges = Vec::new();
116 let mut truncated = false;
117
118 'outer: for ((start_col, end_col), mut span_rows) in spans_by_cols {
119 span_rows.sort_unstable();
120 span_rows.dedup();
121 if span_rows.is_empty() {
122 continue;
123 }
124 let mut run_start = span_rows[0];
125 let mut prev_row = span_rows[0];
126 for row in span_rows.into_iter().skip(1) {
127 if row == prev_row + 1 {
128 prev_row = row;
129 continue;
130 }
131 ranges.push(format_range(start_col, end_col, run_start, prev_row));
132 if ranges.len() >= limit {
133 truncated = true;
134 break 'outer;
135 }
136 run_start = row;
137 prev_row = row;
138 }
139 ranges.push(format_range(start_col, end_col, run_start, prev_row));
140 if ranges.len() >= limit {
141 truncated = true;
142 break;
143 }
144 }
145
146 if truncated {
147 ranges.truncate(limit);
148 }
149 (ranges, truncated)
150}
151
152fn format_range(start_col: u32, end_col: u32, start_row: u32, end_row: u32) -> String {
153 let start_addr = crate::utils::cell_address(start_col, start_row);
154 let end_addr = crate::utils::cell_address(end_col, end_row);
155 if start_addr == end_addr {
156 start_addr
157 } else {
158 format!("{start_addr}:{end_addr}")
159 }
160}
161
162fn descriptor_from_font(font: &Font) -> Option<FontDescriptor> {
163 let bold = *font.get_bold();
164 let italic = *font.get_italic();
165 let underline = font.get_underline();
166 let strikethrough = *font.get_strikethrough();
167 let color = font.get_color().get_argb();
168
169 let descriptor = FontDescriptor {
170 name: Some(font.get_name().to_string()).filter(|s| !s.is_empty()),
171 size: Some(*font.get_size()).filter(|s| *s > 0.0),
172 bold: if bold { Some(true) } else { None },
173 italic: if italic { Some(true) } else { None },
174 underline: if underline.eq_ignore_ascii_case("none") {
175 None
176 } else {
177 Some(underline.to_string())
178 },
179 strikethrough: if strikethrough { Some(true) } else { None },
180 color: Some(color.to_string()).filter(|s| !s.is_empty()),
181 };
182
183 if descriptor.is_empty() {
184 None
185 } else {
186 Some(descriptor)
187 }
188}
189
190fn descriptor_from_fill(fill: &Fill) -> Option<FillDescriptor> {
191 if let Some(pattern) = fill.get_pattern_fill() {
192 let pattern_type = pattern.get_pattern_type();
193 let kind = pattern_type.get_value_string();
194 let fg = pattern
195 .get_foreground_color()
196 .map(|c| c.get_argb().to_string())
197 .filter(|s| !s.is_empty());
198 let bg = pattern
199 .get_background_color()
200 .map(|c| c.get_argb().to_string())
201 .filter(|s| !s.is_empty());
202
203 if kind.eq_ignore_ascii_case("none") && fg.is_none() && bg.is_none() {
204 return None;
205 }
206
207 return Some(FillDescriptor::Pattern(PatternFillDescriptor {
208 pattern_type: if kind.eq_ignore_ascii_case("none") {
209 None
210 } else {
211 Some(kind.to_string())
212 },
213 foreground_color: fg,
214 background_color: bg,
215 }));
216 }
217
218 if let Some(gradient) = fill.get_gradient_fill() {
219 let stops: Vec<GradientStopDescriptor> = gradient
220 .get_gradient_stop()
221 .iter()
222 .map(|stop| GradientStopDescriptor {
223 position: *stop.get_position(),
224 color: stop.get_color().get_argb().to_string(),
225 })
226 .collect();
227
228 let degree = *gradient.get_degree();
229 if stops.is_empty() && degree == 0.0 {
230 return None;
231 }
232
233 return Some(FillDescriptor::Gradient(GradientFillDescriptor {
234 degree: Some(degree).filter(|d| *d != 0.0),
235 stops,
236 }));
237 }
238
239 None
240}
241
242fn descriptor_from_border_side(border: &Border) -> Option<BorderSideDescriptor> {
243 let style = border.get_border_style();
244 let style = if style.eq_ignore_ascii_case("none") {
245 None
246 } else {
247 Some(style.to_string())
248 };
249 let color = Some(border.get_color().get_argb().to_string()).filter(|s| !s.is_empty());
250
251 let descriptor = BorderSideDescriptor { style, color };
252 if descriptor.is_empty() {
253 None
254 } else {
255 Some(descriptor)
256 }
257}
258
259fn descriptor_from_alignment(
260 alignment: &umya_spreadsheet::Alignment,
261) -> Option<AlignmentDescriptor> {
262 let horizontal = if alignment.get_horizontal() != &HorizontalAlignmentValues::General {
263 Some(alignment.get_horizontal().get_value_string().to_string())
264 } else {
265 None
266 };
267 let vertical = if alignment.get_vertical() != &VerticalAlignmentValues::Bottom {
268 Some(alignment.get_vertical().get_value_string().to_string())
269 } else {
270 None
271 };
272 let wrap_text = if *alignment.get_wrap_text() {
273 Some(true)
274 } else {
275 None
276 };
277 let text_rotation = if *alignment.get_text_rotation() != 0 {
278 Some(*alignment.get_text_rotation())
279 } else {
280 None
281 };
282
283 let descriptor = AlignmentDescriptor {
284 horizontal,
285 vertical,
286 wrap_text,
287 text_rotation,
288 };
289 if descriptor.is_empty() {
290 None
291 } else {
292 Some(descriptor)
293 }
294}
295
296trait IsEmpty {
297 fn is_empty(&self) -> bool;
298}
299
300impl IsEmpty for FontDescriptor {
301 fn is_empty(&self) -> bool {
302 self.name.is_none()
303 && self.size.is_none()
304 && self.bold.is_none()
305 && self.italic.is_none()
306 && self.underline.is_none()
307 && self.strikethrough.is_none()
308 && self.color.is_none()
309 }
310}
311
312impl IsEmpty for BorderSideDescriptor {
313 fn is_empty(&self) -> bool {
314 self.style.is_none() && self.color.is_none()
315 }
316}
317
318impl IsEmpty for BordersDescriptor {
319 fn is_empty(&self) -> bool {
320 self.left.is_none()
321 && self.right.is_none()
322 && self.top.is_none()
323 && self.bottom.is_none()
324 && self.diagonal.is_none()
325 && self.vertical.is_none()
326 && self.horizontal.is_none()
327 && self.diagonal_up.is_none()
328 && self.diagonal_down.is_none()
329 }
330}
331
332impl IsEmpty for AlignmentDescriptor {
333 fn is_empty(&self) -> bool {
334 self.horizontal.is_none()
335 && self.vertical.is_none()
336 && self.wrap_text.is_none()
337 && self.text_rotation.is_none()
338 }
339}
340
341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
342pub enum StylePatchMode {
343 Merge,
344 Set,
345 Clear,
346}
347
348pub fn apply_style_patch(current: &Style, patch: &StylePatch, mode: StylePatchMode) -> Style {
349 match mode {
350 StylePatchMode::Clear => Style::default(),
351 StylePatchMode::Set | StylePatchMode::Merge => {
352 let mut desc = match mode {
353 StylePatchMode::Merge => descriptor_from_style(current),
354 StylePatchMode::Set => StyleDescriptor::default(),
355 StylePatchMode::Clear => StyleDescriptor::default(),
356 };
357 merge_style_patch(&mut desc, patch);
358 let mut style = Style::default();
359 apply_descriptor_to_style(&mut style, &desc);
360 style
361 }
362 }
363}
364
365fn merge_style_patch(desc: &mut StyleDescriptor, patch: &StylePatch) {
366 if let Some(font_patch) = &patch.font {
367 match font_patch {
368 None => desc.font = None,
369 Some(p) => {
370 let mut font_desc = desc.font.take().unwrap_or_default();
371 apply_font_patch(&mut font_desc, p);
372 if font_desc.is_empty() {
373 desc.font = None;
374 } else {
375 desc.font = Some(font_desc);
376 }
377 }
378 }
379 }
380
381 if let Some(fill_patch) = &patch.fill {
382 match fill_patch {
383 None => desc.fill = None,
384 Some(p) => {
385 let fill_desc = apply_fill_patch(desc.fill.take(), p);
386 desc.fill = fill_desc;
387 }
388 }
389 }
390
391 if let Some(borders_patch) = &patch.borders {
392 match borders_patch {
393 None => desc.borders = None,
394 Some(p) => {
395 let mut borders_desc = desc.borders.take().unwrap_or_default();
396 apply_borders_patch(&mut borders_desc, p);
397 if borders_desc.is_empty() {
398 desc.borders = None;
399 } else {
400 desc.borders = Some(borders_desc);
401 }
402 }
403 }
404 }
405
406 if let Some(alignment_patch) = &patch.alignment {
407 match alignment_patch {
408 None => desc.alignment = None,
409 Some(p) => {
410 let mut align_desc = desc.alignment.take().unwrap_or_default();
411 apply_alignment_patch(&mut align_desc, p);
412 if align_desc.is_empty() {
413 desc.alignment = None;
414 } else {
415 desc.alignment = Some(align_desc);
416 }
417 }
418 }
419 }
420
421 if let Some(nf_patch) = &patch.number_format {
422 match nf_patch {
423 None => desc.number_format = None,
424 Some(fmt) => {
425 let fmt = fmt.trim();
426 if fmt.is_empty() || fmt.eq_ignore_ascii_case("general") {
427 desc.number_format = None;
428 } else {
429 desc.number_format = Some(fmt.to_string());
430 }
431 }
432 }
433 }
434}
435
436fn apply_font_patch(desc: &mut FontDescriptor, patch: &FontPatch) {
437 apply_double(&mut desc.name, &patch.name);
438 if desc.name.as_deref().is_some_and(|s| s.trim().is_empty()) {
439 desc.name = None;
440 }
441
442 apply_double(&mut desc.size, &patch.size);
443 if desc.size.is_some_and(|s| s <= 0.0) {
444 desc.size = None;
445 }
446
447 apply_double(&mut desc.bold, &patch.bold);
448 if desc.bold == Some(false) {
449 desc.bold = None;
450 }
451
452 apply_double(&mut desc.italic, &patch.italic);
453 if desc.italic == Some(false) {
454 desc.italic = None;
455 }
456
457 apply_double(&mut desc.underline, &patch.underline);
458 if desc
459 .underline
460 .as_deref()
461 .is_some_and(|u| u.trim().is_empty() || u.eq_ignore_ascii_case("none"))
462 {
463 desc.underline = None;
464 }
465
466 apply_double(&mut desc.strikethrough, &patch.strikethrough);
467 if desc.strikethrough == Some(false) {
468 desc.strikethrough = None;
469 }
470
471 apply_double(&mut desc.color, &patch.color);
472 if desc.color.as_deref().is_some_and(|c| c.trim().is_empty()) {
473 desc.color = None;
474 }
475}
476
477fn apply_fill_patch(existing: Option<FillDescriptor>, patch: &FillPatch) -> Option<FillDescriptor> {
478 match patch {
479 FillPatch::Pattern(patch_pattern) => {
480 let mut desc = match existing {
481 Some(FillDescriptor::Pattern(p)) => p,
482 _ => PatternFillDescriptor::default(),
483 };
484 apply_pattern_fill_patch(&mut desc, patch_pattern);
485 if desc.pattern_type.is_none()
486 && desc.foreground_color.is_none()
487 && desc.background_color.is_none()
488 {
489 None
490 } else {
491 Some(FillDescriptor::Pattern(desc))
492 }
493 }
494 FillPatch::Gradient(patch_gradient) => {
495 let mut desc = match existing {
496 Some(FillDescriptor::Gradient(g)) => g,
497 _ => GradientFillDescriptor::default(),
498 };
499 apply_gradient_fill_patch(&mut desc, patch_gradient);
500 if desc.degree.is_none() && desc.stops.is_empty() {
501 None
502 } else {
503 Some(FillDescriptor::Gradient(desc))
504 }
505 }
506 }
507}
508
509fn apply_pattern_fill_patch(desc: &mut PatternFillDescriptor, patch: &PatternFillPatch) {
510 apply_double(&mut desc.pattern_type, &patch.pattern_type);
511 if desc
512 .pattern_type
513 .as_deref()
514 .is_some_and(|p| p.trim().is_empty() || p.eq_ignore_ascii_case("none"))
515 {
516 desc.pattern_type = None;
517 }
518
519 apply_double(&mut desc.foreground_color, &patch.foreground_color);
520 if desc
521 .foreground_color
522 .as_deref()
523 .is_some_and(|c| c.trim().is_empty())
524 {
525 desc.foreground_color = None;
526 }
527
528 apply_double(&mut desc.background_color, &patch.background_color);
529 if desc
530 .background_color
531 .as_deref()
532 .is_some_and(|c| c.trim().is_empty())
533 {
534 desc.background_color = None;
535 }
536}
537
538fn apply_gradient_fill_patch(desc: &mut GradientFillDescriptor, patch: &GradientFillPatch) {
539 apply_double(&mut desc.degree, &patch.degree);
540 if desc.degree == Some(0.0) {
541 desc.degree = None;
542 }
543
544 if let Some(stops) = &patch.stops {
545 desc.stops = stops
546 .iter()
547 .map(|s| GradientStopDescriptor {
548 position: s.position,
549 color: s.color.clone(),
550 })
551 .collect();
552 }
553}
554
555fn apply_borders_patch(desc: &mut BordersDescriptor, patch: &BordersPatch) {
556 apply_border_side_patch(&mut desc.left, &patch.left);
557 apply_border_side_patch(&mut desc.right, &patch.right);
558 apply_border_side_patch(&mut desc.top, &patch.top);
559 apply_border_side_patch(&mut desc.bottom, &patch.bottom);
560 apply_border_side_patch(&mut desc.diagonal, &patch.diagonal);
561 apply_border_side_patch(&mut desc.vertical, &patch.vertical);
562 apply_border_side_patch(&mut desc.horizontal, &patch.horizontal);
563
564 apply_double(&mut desc.diagonal_up, &patch.diagonal_up);
565 if desc.diagonal_up == Some(false) {
566 desc.diagonal_up = None;
567 }
568 apply_double(&mut desc.diagonal_down, &patch.diagonal_down);
569 if desc.diagonal_down == Some(false) {
570 desc.diagonal_down = None;
571 }
572}
573
574fn apply_border_side_patch(
575 target: &mut Option<BorderSideDescriptor>,
576 patch: &Option<Option<BorderSidePatch>>,
577) {
578 match patch {
579 None => {}
580 Some(None) => *target = None,
581 Some(Some(p)) => {
582 let mut side = target.take().unwrap_or_default();
583 apply_double(&mut side.style, &p.style);
584 if side
585 .style
586 .as_deref()
587 .is_some_and(|s| s.trim().is_empty() || s.eq_ignore_ascii_case("none"))
588 {
589 side.style = None;
590 }
591 apply_double(&mut side.color, &p.color);
592 if side.color.as_deref().is_some_and(|c| c.trim().is_empty()) {
593 side.color = None;
594 }
595 if side.is_empty() {
596 *target = None;
597 } else {
598 *target = Some(side);
599 }
600 }
601 }
602}
603
604fn apply_alignment_patch(desc: &mut AlignmentDescriptor, patch: &AlignmentPatch) {
605 apply_double(&mut desc.horizontal, &patch.horizontal);
606 if desc
607 .horizontal
608 .as_deref()
609 .is_some_and(|h| h.trim().is_empty() || h.eq_ignore_ascii_case("general"))
610 {
611 desc.horizontal = None;
612 }
613
614 apply_double(&mut desc.vertical, &patch.vertical);
615 if desc
616 .vertical
617 .as_deref()
618 .is_some_and(|v| v.trim().is_empty() || v.eq_ignore_ascii_case("bottom"))
619 {
620 desc.vertical = None;
621 }
622
623 apply_double(&mut desc.wrap_text, &patch.wrap_text);
624 if desc.wrap_text == Some(false) {
625 desc.wrap_text = None;
626 }
627
628 apply_double(&mut desc.text_rotation, &patch.text_rotation);
629 if desc.text_rotation == Some(0) {
630 desc.text_rotation = None;
631 }
632}
633
634fn apply_descriptor_to_style(style: &mut Style, desc: &StyleDescriptor) {
635 if let Some(font_desc) = &desc.font {
636 let font = style.get_font_mut();
637 if let Some(name) = &font_desc.name {
638 font.set_name(name.clone());
639 }
640 if let Some(size) = font_desc.size {
641 font.set_size(size);
642 }
643 if let Some(bold) = font_desc.bold {
644 font.set_bold(bold);
645 }
646 if let Some(italic) = font_desc.italic {
647 font.set_italic(italic);
648 }
649 if let Some(underline) = &font_desc.underline {
650 font.set_underline(underline.clone());
651 }
652 if let Some(strike) = font_desc.strikethrough {
653 font.set_strikethrough(strike);
654 }
655 if let Some(color) = &font_desc.color {
656 font.get_color_mut().set_argb(color.clone());
657 }
658 }
659
660 if let Some(fill_desc) = &desc.fill {
661 match fill_desc {
662 FillDescriptor::Pattern(p) => {
663 let pat = style.get_fill_mut().get_pattern_fill_mut();
664 if let Some(kind) = &p.pattern_type
665 && let Ok(pv) = PatternValues::from_str(kind)
666 {
667 pat.set_pattern_type(pv);
668 }
669 if let Some(fg) = &p.foreground_color {
670 pat.get_foreground_color_mut().set_argb(fg.clone());
671 }
672 if let Some(bg) = &p.background_color {
673 pat.get_background_color_mut().set_argb(bg.clone());
674 }
675 }
676 FillDescriptor::Gradient(g) => {
677 let grad = style.get_fill_mut().get_gradient_fill_mut();
678 if let Some(deg) = g.degree {
679 grad.set_degree(deg);
680 }
681 grad.get_gradient_stop_mut().clear();
682 for stop in &g.stops {
683 let mut st = umya_spreadsheet::GradientStop::default();
684 st.set_position(stop.position);
685 st.get_color_mut().set_argb(stop.color.clone());
686 grad.set_gradient_stop(st);
687 }
688 }
689 }
690 }
691
692 if let Some(border_desc) = &desc.borders {
693 let borders = style.get_borders_mut();
694 apply_border_side_descriptor(borders.get_left_border_mut(), &border_desc.left);
695 apply_border_side_descriptor(borders.get_right_border_mut(), &border_desc.right);
696 apply_border_side_descriptor(borders.get_top_border_mut(), &border_desc.top);
697 apply_border_side_descriptor(borders.get_bottom_border_mut(), &border_desc.bottom);
698 apply_border_side_descriptor(borders.get_diagonal_border_mut(), &border_desc.diagonal);
699 apply_border_side_descriptor(borders.get_vertical_border_mut(), &border_desc.vertical);
700 apply_border_side_descriptor(borders.get_horizontal_border_mut(), &border_desc.horizontal);
701 if let Some(up) = border_desc.diagonal_up {
702 borders.set_diagonal_up(up);
703 }
704 if let Some(down) = border_desc.diagonal_down {
705 borders.set_diagonal_down(down);
706 }
707 }
708
709 if let Some(align_desc) = &desc.alignment {
710 let align = style.get_alignment_mut();
711 if let Some(h) = &align_desc.horizontal
712 && let Ok(val) = HorizontalAlignmentValues::from_str(h)
713 {
714 align.set_horizontal(val);
715 }
716 if let Some(v) = &align_desc.vertical
717 && let Ok(val) = VerticalAlignmentValues::from_str(v)
718 {
719 align.set_vertical(val);
720 }
721 if let Some(wrap) = align_desc.wrap_text {
722 align.set_wrap_text(wrap);
723 }
724 if let Some(rot) = align_desc.text_rotation {
725 align.set_text_rotation(rot);
726 }
727 }
728
729 if let Some(fmt) = &desc.number_format {
730 style.get_number_format_mut().set_format_code(fmt.clone());
731 }
732}
733
734fn apply_border_side_descriptor(border: &mut Border, desc: &Option<BorderSideDescriptor>) {
735 if let Some(side) = desc {
736 if let Some(style_name) = &side.style {
737 border.set_border_style(style_name.clone());
738 }
739 if let Some(color) = &side.color {
740 border.get_color_mut().set_argb(color.clone());
741 }
742 }
743}
744
745fn apply_double<T: Clone>(target: &mut Option<T>, patch: &Option<Option<T>>) {
746 match patch {
747 None => {}
748 Some(None) => *target = None,
749 Some(Some(v)) => *target = Some(v.clone()),
750 }
751}