1use crate::utils::cell_ref::cell_name_to_coordinates;
8
9const DEFAULT_COMMENT_WIDTH_COLS: u32 = 2;
11const DEFAULT_COMMENT_HEIGHT_ROWS: u32 = 4;
13
14pub fn build_vml_drawing(cells: &[&str]) -> String {
19 let mut shapes = String::new();
20 for (i, cell) in cells.iter().enumerate() {
21 let shape_id = 1025 + i;
22 if let Ok((col, row)) = cell_name_to_coordinates(cell) {
23 let anchor = comment_anchor(col, row);
24 let row_0 = row - 1;
25 let col_0 = col - 1;
26 let z = i + 1;
27 write_vml_shape(&mut shapes, shape_id, z, &anchor, row_0, col_0);
28 }
29 }
30
31 let mut doc = String::with_capacity(1024 + shapes.len());
32 doc.push_str("<xml xmlns:v=\"urn:schemas-microsoft-com:vml\"");
33 doc.push_str(" xmlns:o=\"urn:schemas-microsoft-com:office:office\"");
34 doc.push_str(" xmlns:x=\"urn:schemas-microsoft-com:office:excel\">\n");
35 doc.push_str(" <o:shapelayout v:ext=\"edit\">\n");
36 doc.push_str(" <o:idmap v:ext=\"edit\" data=\"1\"/>\n");
37 doc.push_str(" </o:shapelayout>\n");
38 doc.push_str(" <v:shapetype id=\"_x0000_t202\" coordsize=\"21600,21600\"");
39 doc.push_str(" o:spt=\"202\" path=\"m,l,21600r21600,l21600,xe\">\n");
40 doc.push_str(" <v:stroke joinstyle=\"miter\"/>\n");
41 doc.push_str(" <v:path gradientshapeok=\"t\" o:connecttype=\"rect\"/>\n");
42 doc.push_str(" </v:shapetype>\n");
43 doc.push_str(&shapes);
44 doc.push_str("</xml>\n");
45 doc
46}
47
48fn write_vml_shape(
50 out: &mut String,
51 shape_id: usize,
52 z_index: usize,
53 anchor: &str,
54 row_0: u32,
55 col_0: u32,
56) {
57 use std::fmt::Write;
58 let _ = write!(out, " <v:shape id=\"_x0000_s{}\"", shape_id);
59 out.push_str(" type=\"#_x0000_t202\"");
60 let _ = write!(
61 out,
62 " style=\"position:absolute;margin-left:59.25pt;margin-top:1.5pt;\
63 width:108pt;height:59.25pt;z-index:{};visibility:hidden\"",
64 z_index
65 );
66 out.push_str(" fillcolor=\"#ffffe1\" o:insetmode=\"auto\">\n");
67 out.push_str(" <v:fill color2=\"#ffffe1\"/>\n");
68 out.push_str(" <v:shadow on=\"t\" color=\"black\" obscured=\"t\"/>\n");
69 out.push_str(" <v:path o:connecttype=\"none\"/>\n");
70 out.push_str(" <v:textbox/>\n");
71 out.push_str(" <x:ClientData ObjectType=\"Note\">\n");
72 out.push_str(" <x:MoveWithCells/>\n");
73 out.push_str(" <x:SizeWithCells/>\n");
74 let _ = writeln!(out, " <x:Anchor>{}</x:Anchor>", anchor);
75 let _ = writeln!(out, " <x:Row>{}</x:Row>", row_0);
76 let _ = writeln!(out, " <x:Column>{}</x:Column>", col_0);
77 out.push_str(" </x:ClientData>\n");
78 out.push_str(" </v:shape>\n");
79}
80
81fn comment_anchor(col: u32, row: u32) -> String {
86 let left_col = col;
87 let top_row = if row > 1 { row - 2 } else { 0 };
88 let right_col = col + DEFAULT_COMMENT_WIDTH_COLS;
89 let bottom_row = top_row + DEFAULT_COMMENT_HEIGHT_ROWS;
90 format!("{left_col}, 15, {top_row}, 10, {right_col}, 15, {bottom_row}, 4")
91}
92
93pub fn extract_vml_comment_cells(vml_xml: &str) -> Vec<(u32, u32)> {
98 let mut cells = Vec::new();
99 let mut current_row: Option<u32> = None;
100 let mut current_col: Option<u32> = None;
101
102 for line in vml_xml.lines() {
103 let trimmed = line.trim();
104 if let Some(val) = extract_element_value(trimmed, "x:Row") {
105 current_row = val.parse().ok();
106 }
107 if let Some(val) = extract_element_value(trimmed, "x:Column") {
108 current_col = val.parse().ok();
109 }
110 if current_row.is_some() && current_col.is_some() {
111 cells.push((current_row.unwrap(), current_col.unwrap()));
112 current_row = None;
113 current_col = None;
114 }
115 }
116 cells
117}
118
119fn extract_element_value<'a>(line: &'a str, tag: &str) -> Option<&'a str> {
121 let open = format!("<{}>", tag);
122 let close = format!("</{}>", tag);
123 if let Some(start) = line.find(&open) {
124 let val_start = start + open.len();
125 if let Some(end) = line.find(&close) {
126 return Some(&line[val_start..end]);
127 }
128 }
129 None
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn test_build_vml_drawing_single_cell() {
138 let vml = build_vml_drawing(&["A1"]);
139 assert!(vml.contains("xmlns:v=\"urn:schemas-microsoft-com:vml\""));
140 assert!(vml.contains("<x:Row>0</x:Row>"));
141 assert!(vml.contains("<x:Column>0</x:Column>"));
142 assert!(vml.contains("ObjectType=\"Note\""));
143 assert!(vml.contains("_x0000_t202"));
144 assert!(vml.contains("fillcolor=\"#ffffe1\""));
145 }
146
147 #[test]
148 fn test_build_vml_drawing_multiple_cells() {
149 let vml = build_vml_drawing(&["A1", "C5"]);
150 assert!(vml.contains("<x:Row>0</x:Row>"));
151 assert!(vml.contains("<x:Column>0</x:Column>"));
152 assert!(vml.contains("<x:Row>4</x:Row>"));
153 assert!(vml.contains("<x:Column>2</x:Column>"));
154 assert!(vml.contains("_x0000_s1025"));
155 assert!(vml.contains("_x0000_s1026"));
156 }
157
158 #[test]
159 fn test_build_vml_drawing_empty() {
160 let vml = build_vml_drawing(&[]);
161 assert!(vml.contains("<o:shapelayout"));
162 assert!(vml.contains("<v:shapetype"));
163 assert!(!vml.contains("<v:shape id="));
164 }
165
166 #[test]
167 fn test_extract_vml_comment_cells() {
168 let vml = build_vml_drawing(&["B3", "D10"]);
169 let cells = extract_vml_comment_cells(&vml);
170 assert_eq!(cells.len(), 2);
171 assert_eq!(cells[0], (2, 1));
172 assert_eq!(cells[1], (9, 3));
173 }
174
175 #[test]
176 fn test_comment_anchor_format() {
177 let anchor = comment_anchor(1, 1);
178 assert!(anchor.contains(", "));
179 let parts: Vec<&str> = anchor.split(", ").collect();
180 assert_eq!(parts.len(), 8);
181 }
182
183 #[test]
184 fn test_extract_element_value() {
185 assert_eq!(
186 extract_element_value("<x:Row>5</x:Row>", "x:Row"),
187 Some("5")
188 );
189 assert_eq!(
190 extract_element_value("<x:Column>3</x:Column>", "x:Column"),
191 Some("3")
192 );
193 assert_eq!(extract_element_value("no match here", "x:Row"), None);
194 }
195}