spreadsheet_mcp/
styles.rs

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}