texl_core/template.rs
1//! Template rendering for Texl.
2//!
3//! A template is plain text containing Swan expressions delimited by `<%` and `%>`.
4//! [`render`] replaces each `<% expr %>` tag with the text form of the value that `expr`
5//! evaluates to, and copies all other text through unchanged.
6//!
7//! # Syntax
8//!
9//! - A tag starts at `<%` and ends at the *first* following `%>`. Tags cannot be nested,
10//! and the sequence `%>` cannot appear inside an expression.
11//! - If a `<%` has no matching `%>`, rendering stops at that point and the rest of the
12//! template, including the stray `<%`, is copied through literally.
13//!
14//! # Evaluation model
15//!
16//! - Tags are evaluated strictly in order, all against the same [`Scope`]. Assignments made
17//! in one tag are therefore visible in later tags
18//! (e.g. `<% count = 1 %>Count is <% count %>`).
19//! - If an expression yields a [`Value::Promise`], rendering suspends until the promise
20//! resolves (repeating if the result is itself a promise) and then renders the resolved
21//! value. This is why [`render`] is `async`.
22//! - The future is single-threaded: it holds `Rc` values and is not `Send`.
23//!
24//! # Output format
25//!
26//! | Value | Output |
27//! |------------------------|-----------------------------------------------------|
28//! | `Text` | the text itself |
29//! | `Numb` | Rust `f64` display (`30`, not `30.0`; `1.5`) |
30//! | `Bool` | `TRUE` / `FALSE` |
31//! | `Nothing` | empty text |
32//! | `Tuple` | the items, rendered and joined with `", "` |
33//! | `List` | `[[List of N]]` |
34//! | `Namespace` | `[[Namespace of N]]` (N = number of local bindings) |
35//! | `Func` | `[[Func]]` |
36//! | `Undefined` | `[[Undefined]]` |
37//! | `Promise` (resolved) | the rendered resolved value |
38//! | `Promise` (pending) | `[[Pending Promise]]` |
39//!
40//! Promises returned by a tag are awaited, so `[[Pending Promise]]` only
41//! appears for promises nested inside other values, such as tuple items.
42//!
43//! # Example
44//!
45//! ```
46//! use swan_core::builtins::root_scope;
47//! use swan_core::value::Value;
48//! use texl_core::template::render;
49//!
50//! let scope = root_scope();
51//! scope.set("name", Value::Text("World".into()));
52//! let out = block_on(render("Hello <% name %>!", &scope));
53//! assert_eq!(out, "Hello World!");
54//! # use std::{future::Future, pin::pin, task::{Context, Poll, Waker}};
55//! # fn block_on<F: Future>(fut: F) -> F::Output {
56//! # let mut cx = Context::from_waker(Waker::noop());
57//! # let mut fut = pin!(fut);
58//! # loop {
59//! # texl_core::helpers::run_pending();
60//! # if let Poll::Ready(v) = fut.as_mut().poll(&mut cx) { return v; }
61//! # }
62//! # }
63//! ```
64
65use swan_core::evaluator::eval;
66use swan_core::lexer::Lexer;
67use swan_core::parser::Parser;
68use swan_core::value::{Scope, Value};
69use std::cell::RefCell;
70use std::future::Future;
71use std::pin::Pin;
72use std::rc::Rc;
73use std::task::{Context, Poll, Waker};
74
75
76
77/// State shared between a [`WaitForValue`] future and the promise callback that feeds it.
78///
79/// - `value`: set by the callback when the promise resolves; taken by the future on `poll`.
80/// - `waker`: the most recent waker registered by `poll`; used by the callback to wake the task.
81struct Shared {
82 value: Option<Value>,
83 waker: Option<Waker>,
84}
85
86/// Future that completes once the promise callback has stored a value in the shared state.
87///
88/// Each `poll` either takes the stored value (`Ready`) or registers the current waker
89/// and returns `Pending`.
90struct WaitForValue(Rc<RefCell<Shared>>);
91
92impl Future for WaitForValue {
93 type Output = Value;
94
95 /// Returns `Ready` with the value if the callback has already stored one;
96 /// otherwise stores a clone of `cx`'s waker and returns `Pending`.
97 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Value> {
98 let mut shared = self.0.borrow_mut();
99 if let Some(v) = shared.value.take() {
100 Poll::Ready(v)
101 } else {
102 shared.waker = Some(cx.waker().clone());
103 Poll::Pending
104 }
105 }
106}
107
108
109/// Waits for `val` to become a non-promise value and returns it.
110///
111/// If `val` is a [`Value::Promise`], a callback is registered with `Promise::then`
112/// (which runs immediately if the promise is already resolved) that stores the resolved
113/// value and wakes the awaiting task. This repeats while the resolved value is itself a
114/// promise. Any non-promise value is returned unchanged without suspending.
115///
116/// Promises never reject in Swan, so this function cannot fail.
117async fn resolve_promises(mut val: Value) -> Value {
118 while let Value::Promise(p) = &val {
119 let p = p.clone();
120 let shared = Rc::new(RefCell::new(Shared { value: None, waker: None }));
121 let cb_shared = shared.clone();
122
123 // `then` runs the callback immediately if the promise is already resolved.
124 p.then(move |v| {
125 let waker = {
126 let mut s = cb_shared.borrow_mut();
127 s.value = Some(v);
128 s.waker.take()
129 }; // borrow released before waking
130 if let Some(w) = waker {
131 w.wake();
132 }
133 Value::Nothing
134 });
135
136 let resolved = WaitForValue(shared).await;
137 val = resolved;
138 }
139 val
140}
141
142
143/// Renders `template`, evaluating every `<% ... %>` expression against `scope`.
144///
145/// For each tag, the expression text is lexed, parsed and evaluated with
146/// [`swan_core::evaluator::eval`]. If the result is a promise, it is awaited.
147/// The final value is converted to text (see the [module documentation](self))
148/// and appended to the output. Text outside tags is copied verbatim.
149///
150/// Evaluation mutates `scope`, so assignments persist across tags and after the call returns.
151/// An unclosed `<%` ends processing, and the remaining text is appended literally.
152///
153/// # Arguments
154/// * `template` - the template source text.
155/// * `scope` - the scope used to evaluate all expressions.
156///
157/// # Returns
158/// The fully interpolated string.
159pub async fn render(template: &str, scope: &Scope) -> String {
160 let mut result = String::with_capacity(template.len());
161 let mut cursor = 0;
162
163 while let Some(start_idx) = template[cursor..].find("<%") {
164 let tag_start = cursor + start_idx;
165 let expr_start = tag_start + 2; // skip "<%"
166
167 if let Some(end_idx) = template[expr_start..].find("%>") {
168 let expr_end = expr_start + end_idx;
169 let tag_end = expr_end + 2; // skip "%>"
170
171 // Append literal text prior to the opening tag
172 result.push_str(&template[cursor..tag_start]);
173
174 // Extract expression snippet between delimiters
175 let expr_src = &template[expr_start..expr_end];
176
177 // Parse and evaluate expression
178 let evaluated_val = {
179 let mut parser = Parser::new(Lexer::new(expr_src));
180 let ast = parser.parse();
181 eval(&ast, scope)
182 };
183
184 // If the expression yielded a promise, wait for it to resolve
185 let evaluated_val = resolve_promises(evaluated_val).await;
186
187 // Convert evaluated Value into its text representation
188 result.push_str(&value_to_string(evaluated_val));
189
190 // Move cursor past the closing delimiter tag
191 cursor = tag_end;
192 } else {
193 // Unclosed `<%` delimiter encountered; append remaining text and finish
194 break;
195 }
196 }
197
198 // Append any trailing string fragment after the final tag
199 result.push_str(&template[cursor..]);
200 result
201}
202
203
204/// Converts a Swan [`Value`] into its rendered text form.
205///
206/// | Value | Output |
207/// |------------------------|-----------------------------------------------------|
208/// | `Text` | the string itself |
209/// | `Numb` | Rust `f64` display (`30`, not `30.0`; `1.5`) |
210/// | `Bool` | `TRUE` / `FALSE` |
211/// | `Nothing` | empty string |
212/// | `Tuple` | items rendered and joined with `", "` |
213/// | `List` | `[[List of N]]` |
214/// | `Namespace` | `[[Namespace of N]]` (N = number of local bindings) |
215/// | `Func` / `Builtin` | `[[Func]]` |
216/// | `Undefined` | `[[Undefined]]` |
217/// | `Name` | the name text |
218/// | `Promise` (resolved) | the rendered resolved value |
219/// | `Promise` (pending) | `[[Pending Promise]]` |
220///
221/// Top-level promises are already awaited by [`render`], so `[[Pending Promise]]` can
222/// only appear for promises nested inside other values, such as tuple items.
223fn value_to_string(val: Value) -> String {
224 match val {
225 Value::Text(s) => s.to_string(),
226 Value::Numb(n) => n.to_string(),
227 Value::Bool(b) => if b { "TRUE".into() } else { "FALSE".into() },
228 Value::Nothing => String::new(),
229 Value::List(l) => format!("[[List of {}]]", l.len()),
230 Value::Namespace(s) => format!("[[Namespace of {}]]", s.0.borrow().bindings.len()),
231 Value::Func { .. } | Value::Builtin(_) => "[[Func]]".to_string(),
232 Value::Undefined(_) => "[[Undefined]]".to_string(),
233 Value::Tuple(items) => {
234 let item_strs: Vec<String> = items
235 .iter()
236 .cloned()
237 .map(value_to_string)
238 .collect();
239 item_strs.join(", ")
240 }
241 Value::Name(n) => n.to_string(),
242
243 // Non-blocking: read the promise's current value without awaiting it.
244 Value::Promise(promise) => {
245 let inner_value = promise.0.borrow().value.clone();
246 match inner_value {
247 Some(resolved_val) => value_to_string(resolved_val),
248 None => "[[Pending Promise]]".to_string(),
249 }
250 }
251 }
252}
253
254
255#[cfg(test)]
256/// Unit tests for template rendering, including promise handling.
257mod tests {
258 use super::*;
259 use swan_core::value::{Scope, Value};
260 use swan_core::value::Promise;
261 use std::pin::pin;
262 use std::sync::Arc;
263 use std::task::Wake;
264
265 /// Builds a child scope containing `name` ("World"), `a` (10), `b` (20),
266 /// `is_valid` (TRUE) and `items` (a two-element list).
267 fn create_test_scope() -> Scope {
268 let scope = Scope::new();
269 scope.set("name", Value::Text("World".into()));
270 scope.set("a", Value::Numb(10.0));
271 scope.set("b", Value::Numb(20.0));
272 scope.set("is_valid", Value::Bool(true));
273 scope.set("items", Value::list(vec![Value::Numb(1.0), Value::Numb(2.0)]));
274 scope.new_child()
275 }
276
277 /// Waker that does nothing; sufficient because `block_on` simply re-polls in a loop.
278 struct NoopWake;
279 impl Wake for NoopWake {
280 fn wake(self: Arc<Self>) {}
281 }
282
283 /// Minimal busy-polling executor that drives `fut` to completion on the current thread.
284 fn block_on<F: Future>(fut: F) -> F::Output {
285 let waker = Waker::from(Arc::new(NoopWake));
286 let mut cx = Context::from_waker(&waker);
287 let mut fut = pin!(fut);
288 loop {
289 if let Poll::Ready(v) = fut.as_mut().poll(&mut cx) {
290 return v;
291 }
292 }
293 }
294
295 #[test]
296 /// Text without tags is returned unchanged.
297 fn test_plain_text_without_expressions() {
298 let scope = create_test_scope();
299 let input = "Hello, world! No template tags here.";
300 assert_eq!(block_on(render(input, &scope)), "Hello, world! No template tags here.");
301 }
302
303 #[test]
304 /// A single tag is replaced by its value.
305 fn test_single_expression_interpolation() {
306 let scope = create_test_scope();
307 let input = "Hello <% name %>!";
308 assert_eq!(block_on(render(input, &scope)), "Hello World!");
309 }
310
311 #[test]
312 /// Multiple tags are evaluated, including arithmetic.
313 fn test_multiple_expressions_and_arithmetic() {
314 let scope = create_test_scope();
315 let input = "Sum: <% a + b %>, Product: <% a * b %>";
316 assert_eq!(block_on(render(input, &scope)), "Sum: 30, Product: 200");
317 }
318
319 #[test]
320 /// Booleans render as TRUE/FALSE and Nothing as an empty string.
321 fn test_boolean_and_nothing_formatting() {
322 let scope = create_test_scope();
323 let input = "Valid: <% is_valid %>, Empty: <% () %>";
324 assert_eq!(block_on(render(input, &scope)), "Valid: TRUE, Empty: ");
325 }
326
327 #[test]
328 /// Lists and namespaces render as placeholder summaries.
329 fn test_structured_types_formatting() {
330 let scope = create_test_scope();
331 let input = "List: <% items %>, Namespace: <% { x: 1, y: 2 } %>";
332 assert_eq!(block_on(render(input, &scope)), "List: [[List of 2]], Namespace: [[Namespace of 2]]");
333 }
334
335 #[test]
336 /// Tuple items are joined with ", ".
337 fn test_tuple_formatting() {
338 let scope = create_test_scope();
339 let input = "Tuple: <% (a, b, name) %>";
340 assert_eq!(block_on(render(input, &scope)), "Tuple: 10, 20, World");
341 }
342
343 #[test]
344 /// An unclosed "<%" causes the remaining text to be copied literally.
345 fn test_unclosed_delimiter_tag() {
346 let scope = create_test_scope();
347 let input = "Hello <% name %, remaining text without closing tag <% a";
348 assert_eq!(
349 block_on(render(input, &scope)),
350 "Hello <% name %, remaining text without closing tag <% a"
351 );
352 }
353
354 #[test]
355 /// Assignments in one tag are visible in later tags.
356 fn test_state_mutation_across_tags() {
357 let scope = create_test_scope();
358 let input = "<% count = 1 %>Count is <% count %>, next is <% count = count + 1, count %>";
359 assert_eq!(block_on(render(input, &scope)), "Count is 1, next is 2");
360 }
361
362 #[test]
363 /// An already-resolved promise renders its value without suspending.
364 fn test_already_resolved_promise_is_rendered() {
365 let scope = create_test_scope();
366 let p = Promise::new();
367 p.resolve(Value::Text("done".into()));
368 scope.set("p", Value::Promise(p));
369 assert_eq!(block_on(render("Result: <% p %>", &scope)), "Result: done");
370 }
371
372 #[test]
373 /// A pending promise suspends rendering until it resolves, then renders its value.
374 fn test_pending_promise_is_awaited_then_rendered() {
375 let scope = create_test_scope();
376 let p = Promise::new();
377 scope.set("p", Value::Promise(p.clone()));
378
379 let waker = Waker::from(Arc::new(NoopWake));
380 let mut cx = Context::from_waker(&waker);
381 let mut fut = pin!(render("Value: <% p %>!", &scope));
382
383 // 1. Poll the future once. It should be pending and register the waker.
384 assert!(fut.as_mut().poll(&mut cx).is_pending());
385
386 // 2. Resolve the promise. This should wake the registered waker.
387 p.resolve(Value::Numb(42.0));
388
389 // 3. Poll again until it's ready. The `block_on` utility is perfect for this.
390 let result = block_on(fut);
391 assert_eq!(result, "Value: 42!");
392 }
393}