bnto_shell/args.rs
1// Shell-command argument preparation — conditional arg groups and the
2// `{{url}}` / `{{input}}` placeholders injected by input preparation.
3
4/// Placeholder in args for URL/text input injected by the CLI's input preparation.
5const URL_PLACEHOLDER: &str = "{{url}}";
6const INPUT_PLACEHOLDER: &str = "{{input}}";
7
8/// Resolve `{{url}}` and `{{input}}` placeholders in args from injected params.
9///
10/// The CLI's input preparation injects `"url"` or `"text"` into the node's
11/// params for URL/Text mode recipes. If no placeholder exists in the args
12/// but a `url` param is present, append the URL as the last argument.
13pub(crate) fn resolve_input_placeholders(
14 args: &mut Vec<String>,
15 params: &serde_json::Map<String, serde_json::Value>,
16) {
17 let url = params
18 .get("url")
19 .and_then(serde_json::Value::as_str)
20 .unwrap_or("");
21 let text = params
22 .get("text")
23 .and_then(serde_json::Value::as_str)
24 .unwrap_or("");
25
26 // The input value is whichever was injected (url or text).
27 let input_val = if !url.is_empty() { url } else { text };
28
29 let has_url_placeholder = args.iter().any(|a| a.contains(URL_PLACEHOLDER));
30 let has_input_placeholder = args.iter().any(|a| a.contains(INPUT_PLACEHOLDER));
31
32 // Substitute placeholders in existing args.
33 for arg in args.iter_mut() {
34 if arg.contains(URL_PLACEHOLDER) && !url.is_empty() {
35 *arg = arg.replace(URL_PLACEHOLDER, url);
36 }
37 if arg.contains(INPUT_PLACEHOLDER) && !input_val.is_empty() {
38 *arg = arg.replace(INPUT_PLACEHOLDER, input_val);
39 }
40 }
41
42 // If no placeholder was found but a URL was injected, append it.
43 // This is the common case: `yt-dlp <args...> <url>`
44 if !has_url_placeholder && !has_input_placeholder && !url.is_empty() {
45 args.push(url.to_string());
46 }
47}
48
49/// Flatten a resolved `args` array that may contain conditional groups.
50///
51/// After template resolution, the args array can contain:
52/// - **Flat string**: included if non-empty, dropped if `""`
53/// - **Nested array**: ALL elements must be non-empty strings → flattened
54/// into parent; if ANY element is `""` → entire group dropped
55/// - **Other types**: dropped (backward compat)
56///
57/// This enables conditional flag groups in recipes:
58/// ```json
59/// "args": ["--always", ["--cookies-from-browser", "{{fields.browser}}"], "{{fields.thumbnail}}"]
60/// ```
61/// When `browser=""`: `["--always"]`
62/// When `browser="chrome"`: `["--always", "--cookies-from-browser", "chrome"]`
63pub fn flatten_conditional_args(args: &[serde_json::Value]) -> Vec<String> {
64 let mut result = Vec::new();
65 for item in args {
66 match item {
67 serde_json::Value::String(s) if !s.is_empty() => {
68 result.push(s.clone());
69 }
70 serde_json::Value::Array(group) => {
71 let strings: Vec<&str> =
72 group.iter().filter_map(serde_json::Value::as_str).collect();
73 // All elements must be present strings AND non-empty.
74 let all_non_empty =
75 strings.len() == group.len() && strings.iter().all(|s| !s.is_empty());
76 if all_non_empty {
77 result.extend(strings.into_iter().map(String::from));
78 }
79 }
80 _ => {}
81 }
82 }
83 result
84}