1use crate::base::{CellRef, RangeRef, Value};
23use crate::model::{ColType, InputKind, MAX_MACRO_STEPS, Macro, NamedInput, Step, StepKind, Workbook};
24use serde_json::{Map, Value as Json, json};
25use std::collections::BTreeMap;
26
27use super::policy::{Policy, macro_policy};
28use crate::engine::Selection;
29use crate::engine::record::RecordedStep;
30
31const NEAR_ROWS: u32 = 50;
33const NEAR_COLS: u32 = 26;
34
35#[derive(Clone, Debug)]
38pub struct RecordingStart {
39 pub doc: u64,
40 pub sheet: usize,
41 pub sheet_name: String,
42 pub selection: Selection,
43 pub relative: bool,
44 pub input: Option<NamedInput>,
48 pub name: Option<String>,
50 pub shortcut: Option<String>,
51 pub description: Option<String>,
52}
53
54impl RecordingStart {
55 pub fn new(d: &crate::engine::DocState, relative: bool) -> RecordingStart {
57 let sheet = d.wb.active_sheet();
58 RecordingStart {
59 doc: d.uid,
60 sheet,
61 sheet_name: d.wb.active().map(|sh| sh.name.clone()).unwrap_or_default(),
62 selection: d.selection.clone(),
63 relative,
64 input: relative.then(|| infer_input(&d.wb, sheet, d.selection.current())),
65 name: None,
66 shortcut: None,
67 description: None,
68 }
69 }
70}
71
72pub fn inline_steps(m: &Macro) -> Option<Vec<(String, Json)>> {
76 if !m.inputs.is_empty() || m.output.anchor.is_some() || !m.steps.iter().all(|x| matches!(x.kind, StepKind::Command { .. })) {
77 return None;
78 }
79 Some(
80 m.steps
81 .iter()
82 .filter(|x| x.enabled)
83 .filter_map(|x| match &x.kind {
84 StepKind::Command { id, params } => Some((id.clone(), params.clone())),
85 _ => None,
86 })
87 .collect(),
88 )
89}
90
91#[derive(Clone, Copy, PartialEq, Eq)]
93enum Origin {
94 Recorded,
96 Script,
98 Macro,
100}
101
102pub struct Converted {
104 pub mac: Macro,
105 pub skipped: Vec<String>,
107 pub cut: usize,
110}
111
112pub fn convert(name: &str, start: &RecordingStart, steps: &[RecordedStep], ran: &BTreeMap<usize, Vec<(String, Json)>>) -> Converted {
116 let mut skipped = Vec::new();
117 let mut cmds: Vec<(String, Json, usize, String, Selection, Origin)> = Vec::new();
122 let mut cut = steps.iter().filter(|st| st.doc != start.doc).count();
123 let refused =
124 |id: &str, params: &Json| matches!(macro_policy(id, params, crate::engine::Trust::Local), Policy::Deny(_) | Policy::NeedsPermission(_));
125 let mut push = |cmds: &mut Vec<(String, Json, usize, String, Selection, Origin)>, cmd: (String, Json, usize, String, Selection, Origin)| {
126 if cmds.len() < MAX_MACRO_STEPS {
127 cmds.push(cmd);
128 } else {
129 cut = cut.saturating_add(1);
130 }
131 };
132 for (index, st) in steps.iter().enumerate().filter(|(_, st)| st.doc == start.doc) {
133 let id = st.id.as_str();
134 let origin = match id {
138 "macro.run" => Some(Origin::Macro),
139 "automate.runScript" => Some(Origin::Script),
140 _ => None,
141 };
142 if let Some(origin) = origin {
143 match ran.get(&index) {
144 Some(steps) => {
145 for (cid, params) in steps {
146 if refused(cid, params) {
147 skipped.push(cid.clone());
148 continue;
149 }
150 push(&mut cmds, (cid.clone(), params.clone(), st.sheet, st.sheet_name.clone(), st.selection.clone(), origin));
151 }
152 }
153 None => skipped.push(id.to_string()),
154 }
155 continue;
156 }
157 if refused(id, &st.params) {
158 skipped.push(id.to_string());
159 continue;
160 }
161 push(&mut cmds, (id.to_string(), st.params.clone(), st.sheet, st.sheet_name.clone(), st.selection.clone(), Origin::Recorded));
162 }
163
164 let mut out: Vec<(String, Json, String, Origin)> = Vec::new();
169 let (mut sheet, mut sel) = (start.sheet, start.selection.clone());
170 for (id, params, at_sheet, sheet_name, at_sel, origin) in cmds {
171 if at_sheet != sheet {
172 out.push(("sheet.activate".into(), json!({"sheet": sheet_name}), sheet_name.clone(), Origin::Recorded));
173 }
174 let selected = matches!(out.last(), Some((last, ..)) if last == "selection.set");
176 if (at_sel != sel || at_sheet != sheet) && !selected {
177 out.push(("selection.set".into(), selection_params(&at_sel), sheet_name.clone(), Origin::Recorded));
178 }
179 (sheet, sel) = (at_sheet, at_sel);
180 out.push((id, params, sheet_name, origin));
181 }
182 let mut collapsed: Vec<(String, Json, String, Origin)> = Vec::new();
184 for (id, params, on, origin) in out {
185 if matches!(id.as_str(), "selection.set" | "sheet.activate")
186 && let Some((last, ..)) = collapsed.last()
187 && *last == id
188 {
189 collapsed.pop();
190 }
191 collapsed.push((id, params, on, origin));
192 }
193
194 let example = start.selection.current();
197 let mut used_input = false;
198 let steps: Vec<Step> = collapsed
199 .into_iter()
200 .map(|(id, params, on, origin)| {
201 let names_other_sheet = params.get("sheet").is_some_and(|v| match v {
204 Json::String(n) => !n.eq_ignore_ascii_case(&start.sheet_name),
205 Json::Number(i) => i.as_u64() != Some(start.sheet as u64),
206 _ => false,
207 });
208 let here = on.eq_ignore_ascii_case(&start.sheet_name) && !names_other_sheet;
209 let params = if origin == Origin::Macro { params } else { literal(¶ms, None) };
211 let params = if start.relative && origin == Origin::Recorded {
212 relative(¶ms, example, &start.sheet_name, here, &mut used_input, None)
213 } else {
214 params
215 };
216 Step::command(id, params)
217 })
218 .collect();
219 let mut mac = Macro { name: name.to_string(), steps, ..Macro::default() };
220 if start.relative && used_input {
221 mac.inputs = vec![start.input.clone().unwrap_or_default()];
222 }
223 Converted { mac, skipped, cut }
224}
225
226fn selection_params(sel: &Selection) -> Json {
227 let ranges: Vec<String> = sel.ranges.iter().map(RangeRef::a1).collect();
228 json!({"range": ranges.join(","), "active": sel.active.a1()})
229}
230
231fn literal(v: &Json, key: Option<&str>) -> Json {
235 match v {
236 Json::Object(o) => Json::Object(o.iter().map(|(k, x)| (k.clone(), literal(x, Some(k)))).collect()),
237 Json::Array(a) => Json::Array(a.iter().map(|x| literal(x, key)).collect()),
238 Json::String(t) if key.is_some_and(super::resolve::is_ref_key) && (t.contains("{{") || super::resolve::reads_as_token(t, &["Input"])) => {
239 json!({"$text": t})
240 }
241 Json::String(t) if t.contains("{{") => json!(t.replace("{{", "{{{{")),
242 other => other.clone(),
243 }
244}
245
246fn relative(v: &Json, example: RangeRef, sheet: &str, here: bool, used: &mut bool, key: Option<&str>) -> Json {
249 match v {
250 Json::Object(o) if o.len() == 1 && o.contains_key("$text") => v.clone(),
252 Json::Object(o) => {
253 let several_areas = o.get("range").and_then(Json::as_str).is_some_and(|r| r.contains(','));
256 let mut out = Map::new();
257 for (k, x) in o {
258 let x = if several_areas && k == "active" { x.clone() } else { relative(x, example, sheet, here, used, Some(k)) };
259 out.insert(k.clone(), x);
260 }
261 Json::Object(out)
262 }
263 Json::Array(a) => Json::Array(a.iter().map(|x| relative(x, example, sheet, here, used, key)).collect()),
264 Json::String(t) if key.is_some_and(super::resolve::is_ref_key) => {
265 if t.contains(',') {
267 return v.clone();
268 }
269 match ref_token(t, example, sheet, here) {
270 Some(token) => {
271 *used = true;
272 json!(token)
273 }
274 None => v.clone(),
275 }
276 }
277 Json::String(t) if here && t.starts_with('=') && example.count() > 1 => {
278 let (f, changed) = formula_tokens(t, example);
279 *used |= changed;
280 json!(f)
281 }
282 other => other.clone(),
283 }
284}
285
286fn ref_token(text: &str, example: RangeRef, sheet: &str, here: bool) -> Option<String> {
289 let (on, body) = crate::engine::cmd::split_sheet(text.trim());
290 match on {
291 Some(on) if !on.eq_ignore_ascii_case(sheet) => return None,
292 None if !here => return None,
293 _ => {}
294 }
295 let t = RangeRef::parse(body)?;
296 if t.is_full_cols() || t.is_full_rows() {
297 return None;
298 }
299 token(t, example)
300}
301
302fn token(t: RangeRef, r: RangeRef) -> Option<String> {
304 if t == r {
305 return Some("Input".into());
306 }
307 let rel_row = |row: u32| i64::from(row) - i64::from(r.start.row) + 1;
308 let rel_col = |col: u32| i64::from(col) - i64::from(r.start.col) + 1;
309 let cols = |a: u32, b: u32| if a == b { format!("col({})", rel_col(a)) } else { format!("cols({}..{})", rel_col(a), rel_col(b)) };
310 if r.contains_range(&t) {
311 let full_rows = t.start.row == r.start.row && t.end.row == r.end.row;
312 let full_cols = t.start.col == r.start.col && t.end.col == r.end.col;
313 return Some(if full_rows {
314 format!("Input.{}", cols(t.start.col, t.end.col))
315 } else if full_cols && t.start.row == t.end.row {
316 format!("Input.row({})", rel_row(t.start.row))
317 } else if full_cols && t.end.row == r.end.row {
318 format!("Input.rows({}..)", rel_row(t.start.row))
319 } else if full_cols {
320 format!("Input.rows({}..{})", rel_row(t.start.row), rel_row(t.end.row))
321 } else if t.is_single() {
322 format!("Input.cell({},{})", rel_row(t.start.row), rel_col(t.start.col))
323 } else {
324 format!("Input.cell({},{}).resize({},{})", rel_row(t.start.row), rel_col(t.start.col), t.height(), t.width())
325 });
326 }
327 let near = t.start.row.saturating_add(NEAR_ROWS) >= r.start.row
328 && t.end.row <= r.end.row.saturating_add(NEAR_ROWS)
329 && t.start.col.saturating_add(NEAR_COLS) >= r.start.col
330 && t.end.col <= r.end.col.saturating_add(NEAR_COLS);
331 if !near {
332 return None;
333 }
334 if t.start.row == r.end.row + 1 && t.start.col >= r.start.col && t.end.col <= r.end.col {
336 let base = if t.start.col == r.start.col && t.end.col == r.end.col {
337 "Input".to_string()
338 } else {
339 format!("Input.{}", cols(t.start.col, t.end.col))
340 };
341 return Some(if t.height() == 1 { format!("{base}.below") } else { format!("{base}.below({})", t.height()) });
342 }
343 if t.start.col == r.end.col + 1 && t.start.row >= r.start.row && t.end.row <= r.end.row {
345 let base = if t.start.row == r.start.row && t.end.row == r.end.row {
346 "Input".to_string()
347 } else if t.start.row == t.end.row {
348 format!("Input.row({})", rel_row(t.start.row))
349 } else {
350 format!("Input.rows({}..{})", rel_row(t.start.row), rel_row(t.end.row))
351 };
352 return Some(if t.width() == 1 { format!("{base}.right") } else { format!("{base}.right({})", t.width()) });
353 }
354 let cell = format!("Input.cell({},{})", rel_row(t.start.row), rel_col(t.start.col));
355 Some(if t.is_single() { cell } else { format!("{cell}.resize({},{})", t.height(), t.width()) })
356}
357
358fn formula_tokens(f: &str, r: RangeRef) -> (String, bool) {
361 let mut out = String::with_capacity(f.len());
365 let mut changed = false;
366 let (mut i, mut in_text) = (0, false);
367 let bytes = f.as_bytes();
368 while i < f.len() {
369 let Some(rest) = f.get(i..) else { break };
370 let ch = rest.chars().next().unwrap_or(' ');
371 if ch == '"' {
372 in_text = !in_text;
373 }
374 let prev_ok = i == 0 || !bytes.get(i - 1).is_some_and(|b| b.is_ascii_alphanumeric() || matches!(b, b'$' | b'!' | b'_' | b'.' | b':' | b'{'));
377 if !in_text && prev_ok && ch.is_ascii_alphabetic() {
378 let n = rest.bytes().take_while(|b| b.is_ascii_alphanumeric() || *b == b':').count();
380 let text = rest.get(..n).unwrap_or("");
381 let followed = rest.get(n..).and_then(|x| x.chars().next()).is_some_and(|c| matches!(c, '!' | '(' | '_' | '$'));
382 if !followed
383 && !text.contains('$')
384 && let Some(t) = RangeRef::parse(text)
385 && let Some(tok) = formula_token(t, r)
386 {
387 out.push_str("{{");
388 out.push_str(&tok);
389 out.push_str("}}");
390 i += n;
391 changed = true;
392 continue;
393 }
394 }
395 out.push(ch);
396 i += ch.len_utf8();
397 }
398 (out, changed)
399}
400
401fn formula_token(t: RangeRef, r: RangeRef) -> Option<String> {
404 if t == r {
405 return Some("Input".into());
406 }
407 if !r.contains_range(&t) {
408 return None;
409 }
410 let col = usize::try_from(t.start.col.checked_sub(r.start.col)?).ok()?.checked_add(1)?;
411 let row = usize::try_from(t.start.row.checked_sub(r.start.row)?).ok()?.checked_add(1)?;
412 if t.start.col == t.end.col && t.start.row == r.start.row && t.end.row == r.end.row {
413 return Some(format!("Input.col({col})"));
414 }
415 if t.start.col == t.end.col && r.height() > 2 && t.start.row == r.start.row.checked_add(1)? && t.end.row == r.end.row {
416 return Some(format!("Input.rows(2..).col({col})"));
417 }
418 if r.width() > 1 && t.start.row == t.end.row && t.start.col == r.start.col && t.end.col == r.end.col {
419 return Some(format!("Input.row({row})"));
420 }
421 None
422}
423
424pub fn infer_input(wb: &Workbook, sheet: usize, r: RangeRef) -> NamedInput {
427 let mut input = NamedInput::default();
428 if r.is_single() {
429 input.kind = InputKind::Cell;
430 input.rows = [1, 1];
431 input.cols = [1, 1];
432 return input;
433 }
434 input.cols = [r.width(), r.width()];
435 let Some(sh) = wb.sheet(sheet) else { return input };
436 if r.count() > 100_000 {
437 return input;
438 }
439 let kind = |c: CellRef| -> Option<ColType> {
440 let v = sh.value(c);
441 Some(match v {
442 Value::Empty => return None,
443 Value::Text(t) if t.is_empty() => return None,
444 Value::Number(_) if crate::engine::display::number_format(wb.styles.get(sh.style_id(c)).num_fmt.as_str()).is_date() => ColType::Date,
445 Value::Number(_) => ColType::Number,
446 Value::Text(_) => ColType::Text,
447 Value::Bool(_) => ColType::Bool,
448 _ => ColType::Any,
449 })
450 };
451 let column = |col: u32, from: u32| -> ColType {
452 let mut seen: Option<ColType> = None;
453 for row in from..=r.end.row {
454 match (seen, kind(CellRef::new(row, col))) {
455 (_, None) => {}
456 (None, k) => seen = k,
457 (Some(a), Some(b)) if a != b => return ColType::Any,
458 _ => {}
459 }
460 }
461 seen.unwrap_or(ColType::Any)
462 };
463 let first_text = (r.start.col..=r.end.col).all(|c| kind(CellRef::new(r.start.row, c)) == Some(ColType::Text));
464 let below = (r.start.col..=r.end.col).map(|c| column(c, r.start.row + 1)).collect::<Vec<_>>();
465 input.header = r.height() > 1 && first_text && below.iter().any(|k| !matches!(k, ColType::Text | ColType::Any));
466 input.col_types = if input.header { below } else { (r.start.col..=r.end.col).map(|c| column(c, r.start.row)).collect() };
467 if input.col_types.iter().all(|k| *k == ColType::Any) {
468 input.col_types.clear();
469 }
470 input.rows = [1, 0];
471 input
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477
478 fn r(a1: &str) -> RangeRef {
479 RangeRef::parse(a1).unwrap()
480 }
481
482 #[test]
483 fn references_near_the_example_become_tokens() {
484 let ex = r("B2:D11");
485 for (t, want) in [
486 ("B2:D11", Some("Input")),
487 ("C2:C11", Some("Input.col(2)")),
488 ("C2:D11", Some("Input.cols(2..3)")),
489 ("B2:D2", Some("Input.row(1)")),
490 ("B3:D11", Some("Input.rows(2..)")),
491 ("B3:D5", Some("Input.rows(2..4)")),
492 ("C4", Some("Input.cell(3,2)")),
493 ("C4:D5", Some("Input.cell(3,2).resize(2,2)")),
494 ("B12:D12", Some("Input.below")),
495 ("C12", Some("Input.col(2).below")),
496 ("C12:C13", Some("Input.col(2).below(2)")),
497 ("E2:E11", Some("Input.right")),
498 ("E5", Some("Input.row(4).right")),
499 ("E3:F4", Some("Input.rows(2..3).right(2)")),
500 ("G2", Some("Input.cell(1,6)")),
501 ("A1", Some("Input.cell(0,0)")),
502 ("Z500", None),
503 ("A:A", None),
504 ] {
505 assert_eq!(ref_token(t, ex, "Sheet1", true).as_deref(), want, "{t}");
506 }
507 assert_eq!(ref_token("Sheet1!C4", ex, "Sheet1", true).as_deref(), Some("Input.cell(3,2)"));
508 assert_eq!(ref_token("Other!C4", ex, "Sheet1", true), None);
509 assert_eq!(ref_token("C4", ex, "Sheet1", false), None);
511 assert_eq!(ref_token("Sheet1!C4", ex, "Sheet1", false).as_deref(), Some("Input.cell(3,2)"));
512 }
513
514 #[test]
515 fn formulas_get_tokens_for_exact_references() {
516 let ex = r("B2:C5");
517 for (f, want) in [
518 ("=SUM(C3:C5)", "=SUM({{Input.rows(2..).col(2)}})"),
519 ("=SUM(C2:C5)+COUNT(B2:C5)", "=SUM({{Input.col(2)}})+COUNT({{Input}})"),
520 ("=SUM($C$3:$C$5)", "=SUM($C$3:$C$5)"),
521 ("=SUM(Other!C3:C5)", "=SUM(Other!C3:C5)"),
522 ("=\"C3:C5\"&C3:C5", "=\"C3:C5\"&{{Input.rows(2..).col(2)}}"),
523 ("=SUM(AC3:C5)", "=SUM(AC3:C5)"),
524 ("=B2", "=B2"),
525 ] {
526 assert_eq!(formula_tokens(f, ex).0, want, "{f}");
527 }
528 }
529}