gridline_engine/engine/
preprocess.rs1use regex::Regex;
11
12use super::cell_ref::CellRef;
13
14#[derive(Clone, Copy, Debug)]
16pub enum ShiftOperation {
17 InsertRow(usize),
18 DeleteRow(usize),
19 InsertColumn(usize),
20 DeleteColumn(usize),
21}
22
23pub fn shift_formula_references(formula: &str, op: ShiftOperation) -> String {
31 let mut replacements: Vec<String> = Vec::new();
32 let with_placeholders = crate::builtins::range_fn_re()
34 .replace_all(formula, |caps: ®ex::Captures| {
35 let func_name = &caps[1];
36 let start_ref = &caps[2];
37 let end_ref = &caps[3];
38 let rest_args = caps.get(4).map(|m| m.as_str()).unwrap_or("");
39
40 let new_start = shift_single_ref(start_ref, op);
41 let new_end = shift_single_ref(end_ref, op);
42
43 if new_start == "#REF!" || new_end == "#REF!" {
45 let idx = replacements.len();
46 replacements.push("#REF!".to_string());
47 return format!("@@@{}@@@", idx);
48 }
49
50 let idx = replacements.len();
51 replacements.push(format!(
52 "{}({}:{}{})",
53 func_name, new_start, new_end, rest_args
54 ));
55 format!("@@@{}@@@", idx)
56 })
57 .to_string();
58
59 let shifted = shift_cell_refs_outside_strings(&with_placeholders, op);
61 if replacements.is_empty() {
62 return shifted;
63 }
64
65 let mut restored = shifted;
66 for (idx, replacement) in replacements.into_iter().enumerate() {
67 let placeholder = format!("@@@{}@@@", idx);
68 restored = restored.replace(&placeholder, &replacement);
69 }
70 restored
71}
72
73fn shift_single_ref(cell_ref_str: &str, op: ShiftOperation) -> String {
74 let Some(cr) = CellRef::from_str(cell_ref_str) else {
75 return cell_ref_str.to_string();
76 };
77
78 match op {
79 ShiftOperation::InsertRow(at_row) => {
80 if cr.row >= at_row {
81 CellRef::new(cr.row + 1, cr.col).to_string()
82 } else {
83 cr.to_string()
84 }
85 }
86 ShiftOperation::DeleteRow(at_row) => {
87 if cr.row == at_row {
88 "#REF!".to_string()
89 } else if cr.row > at_row {
90 CellRef::new(cr.row - 1, cr.col).to_string()
91 } else {
92 cr.to_string()
93 }
94 }
95 ShiftOperation::InsertColumn(at_col) => {
96 if cr.col >= at_col {
97 CellRef::new(cr.row, cr.col + 1).to_string()
98 } else {
99 cr.to_string()
100 }
101 }
102 ShiftOperation::DeleteColumn(at_col) => {
103 if cr.col == at_col {
104 "#REF!".to_string()
105 } else if cr.col > at_col {
106 CellRef::new(cr.row, cr.col - 1).to_string()
107 } else {
108 cr.to_string()
109 }
110 }
111 }
112}
113
114fn shift_cell_refs_outside_strings(script: &str, op: ShiftOperation) -> String {
115 let cell_re = Regex::new(r"\b([A-Za-z]+)([0-9]+)\b").unwrap();
116 let value_re = Regex::new(r"@([A-Za-z]+)([0-9]+)\b").unwrap();
117
118 let shift_cells = |seg: &str| {
119 let seg = value_re
121 .replace_all(seg, |caps: ®ex::Captures| {
122 let cell_ref = format!("{}{}", &caps[1], &caps[2]);
123 let shifted = shift_single_ref(&cell_ref, op);
124 if shifted == "#REF!" {
125 shifted
126 } else {
127 format!("@{}", shifted)
128 }
129 })
130 .to_string();
131
132 cell_re
134 .replace_all(&seg, |caps: ®ex::Captures| {
135 let cell_ref = format!("{}{}", &caps[1], &caps[2]);
136 shift_single_ref(&cell_ref, op)
137 })
138 .to_string()
139 };
140
141 let bytes = script.as_bytes();
143 let mut out = String::new();
144 let mut seg_start = 0;
145 let mut in_string = false;
146 let mut backslashes = 0usize;
147 let mut i = 0usize;
148
149 while i < bytes.len() {
150 let b = bytes[i];
151 if in_string {
152 if b == b'\\' {
153 backslashes += 1;
154 i += 1;
155 continue;
156 }
157 if b == b'"' && backslashes % 2 == 0 {
158 out.push_str(&script[seg_start..=i]);
159 in_string = false;
160 seg_start = i + 1;
161 }
162 backslashes = 0;
163 i += 1;
164 continue;
165 }
166
167 if b == b'"' {
168 out.push_str(&shift_cells(&script[seg_start..i]));
169 in_string = true;
170 seg_start = i;
171 backslashes = 0;
172 i += 1;
173 continue;
174 }
175
176 i += 1;
177 }
178
179 if seg_start < script.len() {
180 if in_string {
181 out.push_str(&script[seg_start..]);
182 } else {
183 out.push_str(&shift_cells(&script[seg_start..]));
184 }
185 }
186
187 out
188}
189
190pub fn preprocess_script(script: &str) -> String {
194 preprocess_script_with_context(script, None)
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn test_shift_formula_references_preserves_paren() {
203 let formula = "VEC(A1:A100)";
204 let shifted = shift_formula_references(formula, ShiftOperation::InsertColumn(0));
205 assert_eq!(shifted, "VEC(B1:B100)");
206 }
207
208 #[test]
209 fn test_shift_formula_references_mixed_range_and_cell() {
210 let formula = "SUM(A1:A3) + B1";
211 let shifted = shift_formula_references(formula, ShiftOperation::InsertColumn(0));
212 assert_eq!(shifted, "SUM(B1:B3) + C1");
213 }
214
215 #[test]
216 fn test_shift_formula_references_vec_and_cell() {
217 let formula = "VEC(A1:A10) + B1";
218 let shifted = shift_formula_references(formula, ShiftOperation::InsertColumn(0));
219 assert_eq!(shifted, "VEC(B1:B10) + C1");
220 }
221}
222
223pub fn preprocess_script_with_context(script: &str, context: Option<&CellRef>) -> String {
226 let script = if let Some(cell_ref) = context {
228 let row_re = Regex::new(r"\bROW\(\s*\)").unwrap();
229 let col_re = Regex::new(r"\bCOL\(\s*\)").unwrap();
230 let script = row_re
231 .replace_all(script, (cell_ref.row + 1).to_string())
232 .to_string();
233 col_re
234 .replace_all(&script, (cell_ref.col + 1).to_string())
235 .to_string()
236 } else {
237 script.to_string()
238 };
239
240 preprocess_script_inner(&script)
241}
242
243fn preprocess_script_inner(script: &str) -> String {
244 let with_ranges = crate::builtins::range_fn_re()
245 .replace_all(script, |caps: ®ex::Captures| {
246 let start_ref = &caps[2];
247 let end_ref = &caps[3];
248 let rest_args = caps.get(4).map(|m| m.as_str()).unwrap_or("");
249
250 let Some(rhai_name) = crate::builtins::range_rhai_name(&caps[1]) else {
251 return caps[0].to_string();
252 };
253
254 if let (Some(start), Some(end)) =
255 (CellRef::from_str(start_ref), CellRef::from_str(end_ref))
256 {
257 format!(
258 "{}({}, {}, {}, {}{})",
259 rhai_name, start.row, start.col, end.row, end.col, rest_args
260 )
261 } else {
262 caps[0].to_string()
263 }
264 })
265 .to_string();
266
267 replace_cell_refs_outside_strings(&with_ranges)
268}
269
270fn replace_cell_refs_outside_strings(script: &str) -> String {
271 let cell_re = Regex::new(r"\b([A-Za-z]+)([0-9]+)\b").unwrap();
272 let value_re = Regex::new(r"@([A-Za-z]+)([0-9]+)\b").unwrap();
273
274 let replace_cells = |seg: &str| {
275 let seg = value_re
276 .replace_all(seg, |caps: ®ex::Captures| {
277 let cell_ref = format!("{}{}", &caps[1], &caps[2]);
278 if let Some(cr) = CellRef::from_str(&cell_ref) {
279 format!("value({}, {})", cr.row, cr.col)
280 } else {
281 caps[0].to_string()
282 }
283 })
284 .to_string();
285
286 cell_re
287 .replace_all(&seg, |caps: ®ex::Captures| {
288 let cell_ref = format!("{}{}", &caps[1], &caps[2]);
289 if let Some(cr) = CellRef::from_str(&cell_ref) {
290 format!("cell({}, {})", cr.row, cr.col)
291 } else {
292 caps[0].to_string()
293 }
294 })
295 .to_string()
296 };
297
298 let bytes = script.as_bytes();
299 let mut out = String::new();
300 let mut seg_start = 0;
301 let mut in_string = false;
302 let mut backslashes = 0usize;
303 let mut i = 0usize;
304
305 while i < bytes.len() {
306 let b = bytes[i];
307 if in_string {
308 if b == b'\\' {
309 backslashes += 1;
310 i += 1;
311 continue;
312 }
313 if b == b'"' && backslashes.is_multiple_of(2) {
314 out.push_str(&script[seg_start..=i]);
315 in_string = false;
316 seg_start = i + 1;
317 }
318 backslashes = 0;
319 i += 1;
320 continue;
321 }
322
323 if b == b'"' {
324 out.push_str(&replace_cells(&script[seg_start..i]));
325 in_string = true;
326 seg_start = i;
327 backslashes = 0;
328 i += 1;
329 continue;
330 }
331
332 i += 1;
333 }
334
335 if seg_start < script.len() {
336 if in_string {
337 out.push_str(&script[seg_start..]);
338 } else {
339 out.push_str(&replace_cells(&script[seg_start..]));
340 }
341 }
342
343 out
344}