runjucks_core/
value.rs

1//! JSON [`serde_json::Value`] to display string for template output.
2//!
3//! Also defines internal runtime markers: Nunjucks-style **safe** strings
4//! ([`RJ_SAFE`]) and **undefined** ([`RJ_UNDEFINED`]) for lookup and `default` filter parity.
5
6use crate::globals::{RJ_BUILTIN, RJ_CALLABLE};
7use serde_json::{json, Map, Value};
8use std::borrow::Cow;
9
10/// Object key for HTML-safe output: not re-escaped when [`crate::Environment::autoescape`] is on.
11pub const RJ_SAFE: &str = "__runjucks_safe";
12
13/// Sentinel for “JavaScript `undefined`” (distinct from JSON `null`). Used when a name is not bound
14/// in context or globals, and for `default` filter two-argument semantics.
15pub const RJ_UNDEFINED: &str = "__runjucks_undefined";
16
17/// Object marker for `r/…/…` regex literals (`.test(string)` in [`crate::renderer`]).
18pub const RJ_REGEXP: &str = "__runjucks_regexp";
19
20/// `true` if `v` is a [`mark_safe`] wrapper.
21pub fn is_marked_safe(v: &Value) -> bool {
22    matches!(
23        v,
24        Value::Object(o) if o.get(RJ_SAFE).and_then(|x| x.as_str()).is_some()
25    )
26}
27
28/// `true` if `v` is the internal undefined sentinel ([`undefined_value`]).
29pub fn is_undefined_value(v: &Value) -> bool {
30    matches!(
31        v,
32        Value::Object(o) if o.get(RJ_UNDEFINED) == Some(&Value::Bool(true))
33    )
34}
35
36/// `true` if `v` is a regex literal value (`r/…/…`).
37pub fn is_regexp_value(v: &Value) -> bool {
38    matches!(
39        v,
40        Value::Object(o) if o.get(RJ_REGEXP).and_then(|x| x.as_bool()) == Some(true)
41    )
42}
43
44/// Pattern and flags strings for [`is_regexp_value`] objects.
45pub fn regexp_pattern_flags(v: &Value) -> Option<(String, String)> {
46    let Value::Object(o) = v else {
47        return None;
48    };
49    if o.get(RJ_REGEXP).and_then(|x| x.as_bool()) != Some(true) {
50        return None;
51    }
52    let p = o.get("pattern").and_then(|x| x.as_str())?;
53    let f = o.get("flags").and_then(|x| x.as_str()).unwrap_or("");
54    Some((p.to_string(), f.to_string()))
55}
56
57/// Nunjucks `undefined`-like value for unbound names.
58pub fn undefined_value() -> Value {
59    json!({ RJ_UNDEFINED: true })
60}
61
62fn safe_payload(v: &Value) -> Option<&str> {
63    match v {
64        Value::Object(o) => o.get(RJ_SAFE).and_then(|x| x.as_str()),
65        _ => None,
66    }
67}
68
69/// Wrap a string so autoescape does not re-encode it (Nunjucks `markSafe`).
70pub fn mark_safe(s: String) -> Value {
71    let mut m = Map::new();
72    m.insert(RJ_SAFE.to_string(), Value::String(s));
73    Value::Object(m)
74}
75
76/// Converts a JSON value to its default string form for template output.
77///
78/// | Variant | Output |
79/// |---------|--------|
80/// | [`RJ_UNDEFINED`] sentinel | Empty string |
81/// | [`RJ_SAFE`] wrapper | Inner string |
82/// | [`Value::Null`] | Empty string |
83/// | [`Value::Bool`] | `"true"` or `"false"` |
84/// | [`Value::Number`] | Default numeric string |
85/// | [`Value::String`] | Cloned |
86/// | [`Value::Array`] / plain [`Value::Object`] | JSON `Display` |
87/// User `__runjucks_callable` marker objects (no `__runjucks_builtin`) stringify to empty — like printing a JS function reference without a useful `toString` for templates.
88fn is_empty_callable_marker_object(v: &Value) -> bool {
89    match v {
90        Value::Object(o) => {
91            if o.get(RJ_BUILTIN).is_some() {
92                return false;
93            }
94            o.len() == 1 && o.get(RJ_CALLABLE) == Some(&Value::Bool(true))
95        }
96        _ => false,
97    }
98}
99
100pub fn value_to_string(v: &Value) -> String {
101    if is_undefined_value(v) {
102        return String::new();
103    }
104    if is_empty_callable_marker_object(v) {
105        return String::new();
106    }
107    if let Some(s) = safe_payload(v) {
108        return s.to_string();
109    }
110    match v {
111        Value::Null => String::new(),
112        Value::Bool(b) => b.to_string(),
113        Value::Number(n) => n.to_string(),
114        Value::String(s) => s.clone(),
115        Value::Array(_) | Value::Object(_) => v.to_string(),
116    }
117}
118
119/// Raw string content for escaping (unwraps safe; undefined → empty).
120pub fn value_to_string_raw(v: &Value) -> Cow<'_, str> {
121    if is_undefined_value(v) {
122        return Cow::Borrowed("");
123    }
124    if let Some(s) = safe_payload(v) {
125        return Cow::Borrowed(s);
126    }
127    match v {
128        Value::Null => Cow::Borrowed(""),
129        Value::Bool(b) => Cow::Owned(b.to_string()),
130        Value::Number(n) => Cow::Owned(n.to_string()),
131        Value::String(s) => Cow::Borrowed(s.as_str()),
132        Value::Array(_) | Value::Object(_) => Cow::Owned(v.to_string()),
133    }
134}