Skip to main content

graphix_package_hbs/
lib.rs

1#![doc(
2    html_logo_url = "https://graphix-lang.github.io/graphix/graphix-icon.svg",
3    html_favicon_url = "https://graphix-lang.github.io/graphix/graphix-icon.svg"
4)]
5use anyhow::{bail, Result};
6use arcstr::ArcStr;
7use graphix_compiler::{deref_typ, errf, typ::Type, ExecCtx, PrintFlag, Rt, TypecheckPhase, UserEvent};
8use graphix_package_core::{is_struct, CachedArgs, CachedVals, EvalCached};
9use graphix_package_json::value_to_json;
10use handlebars::Handlebars;
11use netidx::publisher::Typ;
12use netidx_value::Value;
13
14fn is_null_type(t: &Type) -> bool {
15    matches!(t, Type::Primitive(flags) if flags.iter().count() == 1 && flags.contains(Typ::Null))
16}
17
18fn register_partials(
19    registry: &mut Handlebars<'static>,
20    partials: &Value,
21) -> std::result::Result<(), String> {
22    match partials {
23        Value::Null => Ok(()),
24        Value::Array(arr) if is_struct(arr) => {
25            for field in arr.iter() {
26                if let Value::Array(pair) = field {
27                    if let (Value::String(name), Value::String(tmpl)) =
28                        (&pair[0], &pair[1])
29                    {
30                        registry
31                            .register_partial(name.as_str(), tmpl.as_str())
32                            .map_err(|e| format!("{e}"))?;
33                    } else {
34                        return Err(format!(
35                            "partial values must be strings, got {}",
36                            &pair[1]
37                        ));
38                    }
39                }
40            }
41            Ok(())
42        }
43        Value::Map(m) => {
44            for (k, v) in m.into_iter() {
45                match v {
46                    Value::String(tmpl) => {
47                        registry
48                            .register_partial(&format!("{k}"), tmpl.as_str())
49                            .map_err(|e| format!("{e}"))?;
50                    }
51                    _ => return Err(format!("partial values must be strings, got {v}")),
52                }
53            }
54            Ok(())
55        }
56        v => Err(format!("partials must be a struct, map, or null, got {v}")),
57    }
58}
59
60#[derive(Debug)]
61struct HbsRenderEv {
62    registry: Handlebars<'static>,
63    last_template: Option<ArcStr>,
64    last_strict: bool,
65    last_partials: Option<Value>,
66}
67
68impl Default for HbsRenderEv {
69    fn default() -> Self {
70        Self {
71            registry: Handlebars::new(),
72            last_template: None,
73            last_strict: false,
74            last_partials: None,
75        }
76    }
77}
78
79impl<R: Rt, E: UserEvent> EvalCached<R, E> for HbsRenderEv {
80    const NAME: &str = "hbs_render";
81    const NEEDS_CALLSITE: bool = true;
82
83    fn typecheck(
84        &mut self,
85        ctx: &mut ExecCtx<R, E>,
86        _from: &mut [graphix_compiler::Node<R, E>],
87        phase: TypecheckPhase<'_>,
88    ) -> Result<()> {
89        match phase {
90            TypecheckPhase::Lambda => Ok(()),
91            TypecheckPhase::CallSite(resolved) => {
92                if let Some(partials_arg) = resolved.args.get(1) {
93                    deref_typ!("struct, map, or null", ctx, &partials_arg.typ,
94                        Some(Type::Struct(_)) => Ok(()),
95                        Some(Type::Map { .. }) => Ok(()),
96                        Some(t @ Type::Primitive(_)) => {
97                            if is_null_type(t) { Ok(()) }
98                            else { bail!("hbs::render #partials must be a struct, map, or null") }
99                        },
100                        None => Ok(()) // unresolved = using default
101                    )?;
102                }
103                if let Some(data_arg) = resolved.args.get(3) {
104                    deref_typ!("struct or map", ctx, &data_arg.typ,
105                        Some(Type::Struct(_)) => Ok(()),
106                        Some(Type::Map { .. }) => Ok(())
107                    )?;
108                }
109                Ok(())
110            }
111        }
112    }
113
114    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, cached: &CachedVals) -> Option<Value> {
115        let strict = cached.get::<bool>(0)?;
116        let partials = cached.0.get(1)?.clone();
117        let template = match cached.0.get(2)?.as_ref()? {
118            Value::String(s) => s.clone(),
119            _ => return Some(errf!("HbsErr", "template must be a string")),
120        };
121        let data = cached.0.get(3)?.as_ref()?;
122        // rebuild registry if template, strict, or partials changed
123        let template_changed =
124            self.last_template.as_ref().map_or(true, |prev| prev != &template);
125        let strict_changed = self.last_strict != strict;
126        let partials_changed = self.last_partials != partials;
127        if template_changed || strict_changed || partials_changed {
128            self.registry = Handlebars::new();
129            self.registry.set_strict_mode(strict);
130            if let Some(ref p) = partials {
131                if let Err(e) = register_partials(&mut self.registry, p) {
132                    return Some(errf!("HbsErr", "{e}"));
133                }
134            }
135            match self.registry.register_template_string("main", template.as_str()) {
136                Ok(()) => (),
137                Err(e) => return Some(errf!("HbsErr", "{e}")),
138            }
139            self.last_template = Some(template);
140            self.last_strict = strict;
141            self.last_partials = partials;
142        }
143        let json_data = match value_to_json(data) {
144            Ok(j) => j,
145            Err(e) => return Some(errf!("HbsErr", "{e}")),
146        };
147        match self.registry.render("main", &json_data) {
148            Ok(s) => Some(Value::String(ArcStr::from(s.as_str()))),
149            Err(e) => Some(errf!("HbsErr", "{e}")),
150        }
151    }
152}
153
154type HbsRender = CachedArgs<HbsRenderEv>;
155
156graphix_derive::defpackage! {
157    builtins => [
158        HbsRender,
159    ],
160}