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 with_shifted_ranges = crate::builtins::range_fn_re()
33 .replace_all(formula, |caps: ®ex::Captures| {
34 let func_name = &caps[1];
35 let start_ref = &caps[2];
36 let end_ref = &caps[3];
37 let rest_args = caps.get(4).map(|m| m.as_str()).unwrap_or("");
38
39 let new_start = shift_single_ref(start_ref, op);
40 let new_end = shift_single_ref(end_ref, op);
41
42 if new_start == "#REF!" || new_end == "#REF!" {
44 return "#REF!".to_string();
45 }
46
47 format!("{}({}:{}{}", func_name, new_start, new_end, rest_args)
48 })
49 .to_string();
50
51 shift_cell_refs_outside_strings(&with_shifted_ranges, op)
53}
54
55fn shift_single_ref(cell_ref_str: &str, op: ShiftOperation) -> String {
56 let Some(cr) = CellRef::from_str(cell_ref_str) else {
57 return cell_ref_str.to_string();
58 };
59
60 match op {
61 ShiftOperation::InsertRow(at_row) => {
62 if cr.row >= at_row {
63 CellRef::new(cr.row + 1, cr.col).to_string()
64 } else {
65 cr.to_string()
66 }
67 }
68 ShiftOperation::DeleteRow(at_row) => {
69 if cr.row == at_row {
70 "#REF!".to_string()
71 } else if cr.row > at_row {
72 CellRef::new(cr.row - 1, cr.col).to_string()
73 } else {
74 cr.to_string()
75 }
76 }
77 ShiftOperation::InsertColumn(at_col) => {
78 if cr.col >= at_col {
79 CellRef::new(cr.row, cr.col + 1).to_string()
80 } else {
81 cr.to_string()
82 }
83 }
84 ShiftOperation::DeleteColumn(at_col) => {
85 if cr.col == at_col {
86 "#REF!".to_string()
87 } else if cr.col > at_col {
88 CellRef::new(cr.row, cr.col - 1).to_string()
89 } else {
90 cr.to_string()
91 }
92 }
93 }
94}
95
96fn shift_cell_refs_outside_strings(script: &str, op: ShiftOperation) -> String {
97 let cell_re = Regex::new(r"\b([A-Za-z]+)([0-9]+)\b").unwrap();
98 let value_re = Regex::new(r"@([A-Za-z]+)([0-9]+)\b").unwrap();
99
100 let shift_cells = |seg: &str| {
101 let seg = value_re
103 .replace_all(seg, |caps: ®ex::Captures| {
104 let cell_ref = format!("{}{}", &caps[1], &caps[2]);
105 let shifted = shift_single_ref(&cell_ref, op);
106 if shifted == "#REF!" {
107 shifted
108 } else {
109 format!("@{}", shifted)
110 }
111 })
112 .to_string();
113
114 cell_re
116 .replace_all(&seg, |caps: ®ex::Captures| {
117 let cell_ref = format!("{}{}", &caps[1], &caps[2]);
118 shift_single_ref(&cell_ref, op)
119 })
120 .to_string()
121 };
122
123 let bytes = script.as_bytes();
125 let mut out = String::new();
126 let mut seg_start = 0;
127 let mut in_string = false;
128 let mut backslashes = 0usize;
129 let mut i = 0usize;
130
131 while i < bytes.len() {
132 let b = bytes[i];
133 if in_string {
134 if b == b'\\' {
135 backslashes += 1;
136 i += 1;
137 continue;
138 }
139 if b == b'"' && backslashes % 2 == 0 {
140 out.push_str(&script[seg_start..=i]);
141 in_string = false;
142 seg_start = i + 1;
143 }
144 backslashes = 0;
145 i += 1;
146 continue;
147 }
148
149 if b == b'"' {
150 out.push_str(&shift_cells(&script[seg_start..i]));
151 in_string = true;
152 seg_start = i;
153 backslashes = 0;
154 i += 1;
155 continue;
156 }
157
158 i += 1;
159 }
160
161 if seg_start < script.len() {
162 if in_string {
163 out.push_str(&script[seg_start..]);
164 } else {
165 out.push_str(&shift_cells(&script[seg_start..]));
166 }
167 }
168
169 out
170}
171
172pub fn preprocess_script(script: &str) -> String {
176 preprocess_script_with_context(script, None)
177}
178
179pub fn preprocess_script_with_context(script: &str, context: Option<&CellRef>) -> String {
182 let script = if let Some(cell_ref) = context {
184 let row_re = Regex::new(r"\bROW\(\s*\)").unwrap();
185 let col_re = Regex::new(r"\bCOL\(\s*\)").unwrap();
186 let script = row_re
187 .replace_all(script, (cell_ref.row + 1).to_string())
188 .to_string();
189 col_re
190 .replace_all(&script, (cell_ref.col + 1).to_string())
191 .to_string()
192 } else {
193 script.to_string()
194 };
195
196 preprocess_script_inner(&script)
197}
198
199fn preprocess_script_inner(script: &str) -> String {
200 let with_ranges = crate::builtins::range_fn_re()
201 .replace_all(script, |caps: ®ex::Captures| {
202 let start_ref = &caps[2];
203 let end_ref = &caps[3];
204 let rest_args = caps.get(4).map(|m| m.as_str()).unwrap_or("");
205
206 let Some(rhai_name) = crate::builtins::range_rhai_name(&caps[1]) else {
207 return caps[0].to_string();
208 };
209
210 if let (Some(start), Some(end)) =
211 (CellRef::from_str(start_ref), CellRef::from_str(end_ref))
212 {
213 format!(
214 "{}({}, {}, {}, {}{})",
215 rhai_name, start.row, start.col, end.row, end.col, rest_args
216 )
217 } else {
218 caps[0].to_string()
219 }
220 })
221 .to_string();
222
223 replace_cell_refs_outside_strings(&with_ranges)
224}
225
226fn replace_cell_refs_outside_strings(script: &str) -> String {
227 let cell_re = Regex::new(r"\b([A-Za-z]+)([0-9]+)\b").unwrap();
228 let value_re = Regex::new(r"@([A-Za-z]+)([0-9]+)\b").unwrap();
229
230 let replace_cells = |seg: &str| {
231 let seg = value_re
232 .replace_all(seg, |caps: ®ex::Captures| {
233 let cell_ref = format!("{}{}", &caps[1], &caps[2]);
234 if let Some(cr) = CellRef::from_str(&cell_ref) {
235 format!("value({}, {})", cr.row, cr.col)
236 } else {
237 caps[0].to_string()
238 }
239 })
240 .to_string();
241
242 cell_re
243 .replace_all(&seg, |caps: ®ex::Captures| {
244 let cell_ref = format!("{}{}", &caps[1], &caps[2]);
245 if let Some(cr) = CellRef::from_str(&cell_ref) {
246 format!("cell({}, {})", cr.row, cr.col)
247 } else {
248 caps[0].to_string()
249 }
250 })
251 .to_string()
252 };
253
254 let bytes = script.as_bytes();
255 let mut out = String::new();
256 let mut seg_start = 0;
257 let mut in_string = false;
258 let mut backslashes = 0usize;
259 let mut i = 0usize;
260
261 while i < bytes.len() {
262 let b = bytes[i];
263 if in_string {
264 if b == b'\\' {
265 backslashes += 1;
266 i += 1;
267 continue;
268 }
269 if b == b'"' && backslashes.is_multiple_of(2) {
270 out.push_str(&script[seg_start..=i]);
271 in_string = false;
272 seg_start = i + 1;
273 }
274 backslashes = 0;
275 i += 1;
276 continue;
277 }
278
279 if b == b'"' {
280 out.push_str(&replace_cells(&script[seg_start..i]));
281 in_string = true;
282 seg_start = i;
283 backslashes = 0;
284 i += 1;
285 continue;
286 }
287
288 i += 1;
289 }
290
291 if seg_start < script.len() {
292 if in_string {
293 out.push_str(&script[seg_start..]);
294 } else {
295 out.push_str(&replace_cells(&script[seg_start..]));
296 }
297 }
298
299 out
300}