Skip to main content

ctl_core/surface/
templates.rs

1//! `MiniJinja` rendering for a CLI's generated documentation files.
2
3use minijinja::{Environment, Error, context};
4use serde::Serialize;
5
6use super::Surface;
7
8/// Template source for `ctl/version.md.jinja`: the macro
9/// `version_line(version)` renders the `version:` line of a generated file's
10/// frontmatter.
11pub const VERSION_FRAGMENT: &str = r"{% macro version_line(version) -%}
12version: {{ version }}
13{%- endmacro %}";
14
15/// Template source for `ctl/invocation.md.jinja`: the macro
16/// `mounted_invocation(surface, examples)` renders example `mise run` lines
17/// and the rule that they take no `--`.
18pub const INVOCATION_FRAGMENT: &str = r"{% macro mounted_invocation(surface, examples) -%}
19## Invocation
20
21```sh
22{% for example in examples -%}
23mise run {{ surface.mount }} {{ example }}
24{% endfor -%}
25```
26
27Never `mise run {{ surface.mount }} --`.
28
29The `--` in `#USAGE mount` is mise's completion bootstrap.
30{%- endmacro %}";
31
32/// Template source for `ctl/commands.md.jinja`: the macro
33/// `command_inventory(surface)` renders a table of the visible top-level
34/// commands, with their visible aliases and descriptions.
35pub const COMMANDS_FRAGMENT: &str = r#"{% macro command_inventory(surface) -%}
36## Commands
37
38| Command | Aliases | Purpose |
39|:--|:--|:--|
40{% for command in surface.commands if not command.hidden -%}
41| `{{ command.name }}` | {% if command.visible_aliases %}`{{ command.visible_aliases | join("`, `") }}`{% else %}—{% endif %} | {{ command.about | replace("|", "\\|") | replace("\n", " ") }} |
42{% endfor -%}
43{{- "" -}}
44{%- endmacro %}"#;
45
46const FRAGMENTS: [(&str, &str); 3] = [
47    ("ctl/version.md.jinja", VERSION_FRAGMENT),
48    ("ctl/invocation.md.jinja", INVOCATION_FRAGMENT),
49    ("ctl/commands.md.jinja", COMMANDS_FRAGMENT),
50];
51
52/// Add ctl-core's shared fragments to `environment`, under the names
53/// `ctl/version.md.jinja`, `ctl/invocation.md.jinja`, and
54/// `ctl/commands.md.jinja`. A template imports them from there.
55///
56/// # Errors
57///
58/// Returns minijinja's [`Error`] when a fragment does not compile under
59/// `environment`'s syntax.
60pub fn add_fragments(environment: &mut Environment<'static>) -> Result<(), Error> {
61    for (name, source) in FRAGMENTS {
62        environment.add_template(name, source)?;
63    }
64    Ok(())
65}
66
67/// Create an [`Environment`] that holds the shared fragments.
68///
69/// It is strict: an undefined variable is an error, not an empty string. It
70/// keeps the trailing newline of a template.
71///
72/// # Errors
73///
74/// Returns the [`Error`] from [`add_fragments`].
75pub fn environment() -> Result<Environment<'static>, Error> {
76    let mut environment = Environment::new();
77    environment.set_undefined_behavior(minijinja::UndefinedBehavior::Strict);
78    environment.set_keep_trailing_newline(true);
79    add_fragments(&mut environment)?;
80    Ok(environment)
81}
82
83/// Render the template `source`, named `name`, in a fresh
84/// [`environment`].
85///
86/// The template sees two variables: `surface`, the [`Surface`], and `content`,
87/// the CLI's own serializable context.
88///
89/// # Errors
90///
91/// Returns minijinja's [`Error`] when `source` does not compile, or when
92/// rendering fails, such as on a variable the strict environment does not
93/// define.
94pub fn render<T: Serialize>(
95    name: &'static str,
96    source: &'static str,
97    surface: &Surface,
98    content: &T,
99) -> Result<String, Error> {
100    let mut environment = environment()?;
101    environment.add_template(name, source)?;
102    environment
103        .get_template(name)?
104        .render(context! { surface, content })
105}