1use std::collections::HashMap;
4use std::path::Path;
5
6use anyhow::Error;
7use handlebars::{
8 handlebars_helper, Context, Decorator, DirectorySourceOptions, Handlebars, Helper, HelperDef,
9 HelperResult, Output, RenderContext, RenderError, RenderErrorReason, Renderable,
10};
11
12use crate::format::Formatter;
13
14type FormatterRef<'a> = &'a (dyn Formatter + Send + Sync);
15
16pub fn expand(file: &Path, formatter: FormatterRef<'_>) -> Result<String, Error> {
18 let mut handlebars = Handlebars::new();
19 handlebars.set_strict_mode(true);
20 handlebars.register_helper("lower", Box::new(lower));
21 handlebars.register_helper("options", Box::new(OptionsHelper { formatter }));
22 handlebars.register_helper("option", Box::new(OptionHelper { formatter }));
23 handlebars.register_helper("man", Box::new(ManLinkHelper { formatter }));
24 handlebars.register_decorator("set", Box::new(set_decorator));
25 handlebars.register_template_file("template", file)?;
26 let includes = file.parent().unwrap().join("includes");
27 let mut options = DirectorySourceOptions::default();
28 options.tpl_extension = ".md".to_string();
29 handlebars.register_templates_directory(includes, options)?;
30 let man_name = file
31 .file_stem()
32 .expect("expected filename")
33 .to_str()
34 .expect("utf8 filename")
35 .to_string();
36 let data = HashMap::from([("man_name", man_name)]);
37 let expanded = handlebars.render("template", &data)?;
38 Ok(expanded)
39}
40
41struct OptionsHelper<'a> {
43 formatter: FormatterRef<'a>,
44}
45
46impl HelperDef for OptionsHelper<'_> {
47 fn call<'reg: 'rc, 'rc>(
48 &self,
49 h: &Helper<'rc>,
50 r: &'reg Handlebars<'reg>,
51 ctx: &'rc Context,
52 rc: &mut RenderContext<'reg, 'rc>,
53 out: &mut dyn Output,
54 ) -> HelperResult {
55 if in_options(rc) {
56 return Err(
57 RenderErrorReason::Other("options blocks cannot be nested".to_string()).into(),
58 );
59 }
60 set_in_context(rc, "__MDMAN_IN_OPTIONS", serde_json::Value::Bool(true));
62 let s = self.formatter.render_options_start();
63 out.write(&s)?;
64 let t = match h.template() {
65 Some(t) => t,
66 None => {
67 return Err(RenderErrorReason::Other(
68 "options block must not be empty".to_string(),
69 )
70 .into());
71 }
72 };
73 let block = t.renders(r, ctx, rc)?;
74 out.write(&block)?;
75
76 let s = self.formatter.render_options_end();
77 out.write(&s)?;
78 remove_from_context(rc, "__MDMAN_IN_OPTIONS");
79 Ok(())
80 }
81}
82
83fn in_options(rc: &RenderContext<'_, '_>) -> bool {
85 rc.context()
86 .map_or(false, |ctx| ctx.data().get("__MDMAN_IN_OPTIONS").is_some())
87}
88
89struct OptionHelper<'a> {
91 formatter: FormatterRef<'a>,
92}
93
94impl HelperDef for OptionHelper<'_> {
95 fn call<'reg: 'rc, 'rc>(
96 &self,
97 h: &Helper<'rc>,
98 r: &'reg Handlebars<'reg>,
99 gctx: &'rc Context,
100 rc: &mut RenderContext<'reg, 'rc>,
101 out: &mut dyn Output,
102 ) -> HelperResult {
103 if !in_options(rc) {
104 return Err(
105 RenderErrorReason::Other("option must be in options block".to_string()).into(),
106 );
107 }
108 let params = h.params();
109 if params.is_empty() {
110 return Err(RenderErrorReason::Other(
111 "option block must have at least one param".to_string(),
112 )
113 .into());
114 }
115 let params = params
117 .iter()
118 .map(|param| {
119 param
120 .value()
121 .as_str()
122 .ok_or_else(|| {
123 RenderErrorReason::Other("option params must be strings".to_string())
124 })
125 .into()
126 })
127 .collect::<Result<Vec<&str>, RenderErrorReason>>()?;
128 let t = match h.template() {
129 Some(t) => t,
130 None => {
131 return Err(
132 RenderErrorReason::Other("option block must not be empty".to_string()).into(),
133 );
134 }
135 };
136 let block = t.renders(r, gctx, rc)?;
138
139 let block = block.replace("\r\n", "\n");
141
142 let man_name = gctx
144 .data()
145 .get("man_name")
146 .expect("expected man_name in context")
147 .as_str()
148 .expect("expect man_name str");
149
150 let option = self
152 .formatter
153 .render_option(¶ms, &block, man_name)
154 .map_err(|e| RenderErrorReason::Other(format!("option render failed: {}", e)))?;
155 out.write(&option)?;
156 Ok(())
157 }
158}
159
160struct ManLinkHelper<'a> {
162 formatter: FormatterRef<'a>,
163}
164
165impl HelperDef for ManLinkHelper<'_> {
166 fn call<'reg: 'rc, 'rc>(
167 &self,
168 h: &Helper<'rc>,
169 _r: &'reg Handlebars<'reg>,
170 _ctx: &'rc Context,
171 _rc: &mut RenderContext<'reg, 'rc>,
172 out: &mut dyn Output,
173 ) -> HelperResult {
174 let params = h.params();
175 if params.len() != 2 {
176 return Err(
177 RenderErrorReason::Other("{{man}} must have two arguments".to_string()).into(),
178 );
179 }
180 let name = params[0].value().as_str().ok_or_else(|| {
181 RenderErrorReason::Other("man link name must be a string".to_string())
182 })?;
183 let section = params[1].value().as_u64().ok_or_else(|| {
184 RenderErrorReason::Other("man link section must be an integer".to_string())
185 })?;
186 let section = u8::try_from(section)
187 .map_err(|_e| RenderErrorReason::Other("section number too large".to_string()))?;
188 let link = self
189 .formatter
190 .linkify_man_to_md(name, section)
191 .map_err(|e| RenderErrorReason::Other(format!("failed to linkify man: {}", e)))?;
192 out.write(&link)?;
193 Ok(())
194 }
195}
196
197fn set_decorator(
201 d: &Decorator<'_>,
202 _: &Handlebars<'_>,
203 _ctx: &Context,
204 rc: &mut RenderContext<'_, '_>,
205) -> Result<(), RenderError> {
206 let data_to_set = d.hash();
207 for (k, v) in data_to_set {
208 set_in_context(rc, k, v.value().clone());
209 }
210 Ok(())
211}
212
213fn set_in_context(rc: &mut RenderContext<'_, '_>, key: &str, value: serde_json::Value) {
215 let mut gctx = match rc.context() {
216 Some(c) => (*c).clone(),
217 None => Context::wraps(serde_json::Value::Object(serde_json::Map::new())).unwrap(),
218 };
219 if let serde_json::Value::Object(m) = gctx.data_mut() {
220 m.insert(key.to_string(), value);
221 rc.set_context(gctx);
222 } else {
223 panic!("expected object in context");
224 }
225}
226
227fn remove_from_context(rc: &mut RenderContext<'_, '_>, key: &str) {
229 let gctx = rc.context().expect("cannot remove from null context");
230 let mut gctx = (*gctx).clone();
231 if let serde_json::Value::Object(m) = gctx.data_mut() {
232 m.remove(key);
233 rc.set_context(gctx);
234 } else {
235 panic!("expected object in context");
236 }
237}
238
239handlebars_helper!(lower: |s: str| s.to_lowercase());