1use crate::errors::{Result, RunjucksError};
6use serde_json::{json, Map, Value};
7use std::collections::HashMap;
8
9pub const RJ_BUILTIN: &str = "__runjucks_builtin";
11
12pub const RJ_CYCLER: &str = "__runjucks_cycler";
14
15pub const RJ_JOINER: &str = "__runjucks_joiner";
17
18pub const RJ_CALLABLE: &str = "__runjucks_callable";
20
21pub fn builtin_marker(name: &str) -> Value {
23 let mut m = Map::new();
24 m.insert(RJ_BUILTIN.to_string(), Value::String(name.to_string()));
25 Value::Object(m)
26}
27
28pub fn default_globals_map() -> HashMap<String, Value> {
29 ["range", "cycler", "joiner"]
30 .into_iter()
31 .map(|n| (n.to_string(), builtin_marker(n)))
32 .collect()
33}
34
35pub fn value_is_callable(v: &Value) -> bool {
38 match v {
39 Value::Object(o) => o.contains_key(RJ_BUILTIN) || o.contains_key(RJ_CALLABLE),
40 _ => false,
41 }
42}
43
44pub fn is_builtin_marker_value(v: &Value, expected: &str) -> bool {
46 match v {
47 Value::Object(o) => o
48 .get(RJ_BUILTIN)
49 .and_then(|x| x.as_str())
50 .map(|s| s == expected)
51 .unwrap_or(false),
52 _ => false,
53 }
54}
55
56fn as_f64(v: &Value) -> Result<f64> {
57 match v {
58 Value::Number(n) => n
59 .as_f64()
60 .ok_or_else(|| RunjucksError::new("invalid number in `range`")),
61 _ => Err(RunjucksError::new("`range` expects numeric arguments")),
62 }
63}
64
65pub fn builtin_range(args: &[Value]) -> Result<Value> {
67 let (start, stop, step) = match args.len() {
68 0 => {
69 return Err(RunjucksError::new("`range` expects at least one argument"));
70 }
71 1 => (0.0, as_f64(&args[0])?, 1.0),
72 2 => (as_f64(&args[0])?, as_f64(&args[1])?, 1.0),
73 3 => (
74 as_f64(&args[0])?,
75 as_f64(&args[1])?,
76 if args[2].is_null() {
77 1.0
78 } else {
79 as_f64(&args[2])?
80 },
81 ),
82 _ => {
83 return Err(RunjucksError::new(
84 "`range` expects at most three arguments",
85 ));
86 }
87 };
88
89 if step == 0.0 {
90 return Err(RunjucksError::new("`range` step cannot be zero"));
91 }
92
93 let mut out = Vec::new();
94 if step > 0.0 {
95 let mut i = start;
96 while i < stop {
97 out.push(json_num(i));
98 i += step;
99 }
100 } else {
101 let mut i = start;
102 while i > stop {
103 out.push(json_num(i));
104 i += step;
105 }
106 }
107 Ok(Value::Array(out))
108}
109
110fn json_num(x: f64) -> Value {
111 if x.fract() == 0.0 && x >= i64::MIN as f64 && x <= i64::MAX as f64 {
112 json!(x as i64)
113 } else {
114 json!(x)
115 }
116}
117
118#[derive(Debug)]
120pub struct CyclerState {
121 pub items: Vec<Value>,
122 pos: isize,
124}
125
126impl CyclerState {
127 pub fn new(items: Vec<Value>) -> Self {
128 Self { items, pos: -1 }
129 }
130
131 #[allow(clippy::should_implement_trait)]
133 pub fn next(&mut self) -> Value {
134 if self.items.is_empty() {
135 return Value::Null;
136 }
137 self.pos += 1;
138 if self.pos >= self.items.len() as isize {
139 self.pos = 0;
140 }
141 self.items[self.pos as usize].clone()
142 }
143}
144
145#[derive(Debug)]
147pub struct JoinerState {
148 pub sep: String,
149 first: bool,
150}
151
152impl JoinerState {
153 pub fn new(sep: String) -> Self {
154 Self { sep, first: true }
155 }
156
157 pub fn invoke(&mut self) -> String {
158 if self.first {
159 self.first = false;
160 String::new()
161 } else {
162 self.sep.clone()
163 }
164 }
165}
166
167pub fn cycler_handle_value(id: usize) -> Value {
168 json!({ RJ_CYCLER: id })
169}
170
171pub fn joiner_handle_value(id: usize) -> Value {
172 json!({ RJ_JOINER: id })
173}
174
175pub fn parse_cycler_id(v: &Value) -> Option<usize> {
176 match v {
177 Value::Object(o) => o
178 .get(RJ_CYCLER)
179 .and_then(|x| x.as_u64())
180 .map(|x| x as usize),
181 _ => None,
182 }
183}
184
185pub fn parse_joiner_id(v: &Value) -> Option<usize> {
186 match v {
187 Value::Object(o) => o
188 .get(RJ_JOINER)
189 .and_then(|x| x.as_u64())
190 .map(|x| x as usize),
191 _ => None,
192 }
193}