graphix_package_hbs/
lib.rs1#![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(()) )?;
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 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}