#
mdman/
hbs.rs

    1//! Handlebars template processing.
    2
    3use std::collections::HashMap;
    4use std::path::Path;
    5
    6use anyhow::Error;
    7use handlebars::{
    8    Context, Decorator, DirectorySourceOptions, Handlebars, Helper, HelperDef, HelperResult,
    9    Output, RenderContext, RenderError, RenderErrorReason, Renderable, handlebars_helper,
    10};
    11
    12use crate::format::Formatter;
    13
    14type FormatterRef<'a> = &'a (dyn Formatter + Send + Sync);
    15
    16/// Processes the handlebars template at the given file.
    17pub 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
    41/// Helper for `{{#options}}` block.
    42struct 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        // Prevent nested {{#options}}.
    61        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
    83/// Whether or not the context is currently inside a `{{#options}}` block.
    84fn in_options(rc: &RenderContext<'_, '_>) -> bool {
    85    rc.context()
    86        .map_or(false, |ctx| ctx.data().get("__MDMAN_IN_OPTIONS").is_some())
    87}
    88
    89/// Helper for `{{#option}}` block.
    90struct 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        // Convert params to strings.
    116        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        // Render the block.
    137        let block = t.renders(r, gctx, rc)?;
    138
    139        // Windows newlines can break some rendering, so normalize.
    140        let block = block.replace("\r\n", "\n");
    141
    142        // Get the name of this page.
    143        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        // Ask the formatter to convert this option to its format.
    151        let option = self
    152            .formatter
    153            .render_option(&params, &block, man_name)
    154            .map_err(|e| RenderErrorReason::Other(format!("option render failed: {}", e)))?;
    155        out.write(&option)?;
    156        Ok(())
    157    }
    158}
    159
    160/// Helper for `{{man name section}}` expression.
    161struct 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
    197/// `{{*set var=value}}` decorator.
    198///
    199/// This sets a variable to a value within the template context.
    200fn 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
    213/// Sets a variable to a value within the context.
    214fn 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
    227/// Removes a variable from the context.
    228fn 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());