Skip to main content

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}