Skip to main content

usage/docs/markdown/
renderer.rs

1use crate::docs::markdown::tera::TERA;
2use crate::docs::models::Spec;
3use crate::error::UsageErr;
4use itertools::Itertools;
5use serde::Serialize;
6
7#[derive(Debug, Clone)]
8pub struct MarkdownRenderer {
9    pub(crate) spec: Spec,
10    pub(crate) header_level: usize,
11    pub(crate) multi: bool,
12    tera_ctx: tera::Context,
13    url_prefix: Option<String>,
14    html_encode: bool,
15    replace_pre_with_code_fences: bool,
16}
17
18impl MarkdownRenderer {
19    pub fn new(spec: crate::Spec) -> Self {
20        let mut renderer = Self {
21            spec: spec.into(),
22            header_level: 1,
23            multi: false,
24            tera_ctx: tera::Context::new(),
25            url_prefix: None,
26            html_encode: true,
27            replace_pre_with_code_fences: false,
28        };
29        let mut spec = renderer.spec.clone();
30        spec.render_md(&renderer);
31        renderer.spec = spec;
32        renderer
33    }
34
35    pub fn with_header_level(mut self, header_level: usize) -> Self {
36        self.header_level = header_level;
37        self
38    }
39
40    pub fn with_multi(mut self, index: bool) -> Self {
41        self.multi = index;
42        self
43    }
44
45    pub fn with_url_prefix<S: Into<String>>(mut self, url_prefix: S) -> Self {
46        self.url_prefix = Some(url_prefix.into());
47        self
48    }
49
50    pub fn with_html_encode(mut self, html_encode: bool) -> Self {
51        self.html_encode = html_encode;
52        self
53    }
54
55    pub fn with_replace_pre_with_code_fences(mut self, replace_pre_with_code_fences: bool) -> Self {
56        self.replace_pre_with_code_fences = replace_pre_with_code_fences;
57        self
58    }
59
60    pub(crate) fn insert<T: Serialize + ?Sized, S: Into<String>>(&mut self, key: S, val: &T) {
61        self.tera_ctx.insert(key.into(), val);
62    }
63
64    fn tera_ctx(&self) -> tera::Context {
65        let mut ctx = self.tera_ctx.clone();
66        ctx.insert("spec", &self.spec);
67        ctx.insert("header_level", &self.header_level);
68        ctx.insert("multi", &self.multi);
69        ctx.insert("url_prefix", &self.url_prefix);
70        ctx.insert("html_encode", &self.html_encode);
71        ctx
72    }
73
74    pub(crate) fn render(&self, template_name: &str) -> Result<String, UsageErr> {
75        let mut tera = TERA.clone();
76
77        let html_encode = self.html_encode;
78        tera.register_filter(
79            "escape_md",
80            move |value: &tera::Value,
81                  _: tera::Kwargs,
82                  _: &tera::State|
83                  -> tera::TeraResult<String> {
84                let value = value.as_str().unwrap();
85                let value = value
86                    .lines()
87                    .map(|line| {
88                        if !html_encode || line.starts_with("    ") {
89                            return line.to_string();
90                        }
91                        // replace '<' with '&lt;' but not inside code blocks
92                        xx::regex!(r"(`[^`]*`)|(<)")
93                            .replace_all(line, |caps: &regex::Captures| {
94                                if caps.get(1).is_some() {
95                                    caps.get(1).unwrap().as_str().to_string()
96                                } else {
97                                    "&lt;".to_string()
98                                }
99                            })
100                            .to_string()
101                    })
102                    .join("\n");
103                Ok(value)
104            },
105        );
106
107        Ok(tera.render(template_name, &self.tera_ctx())?)
108    }
109
110    pub(crate) fn replace_code_fences(&self, md: String) -> String {
111        if !self.replace_pre_with_code_fences {
112            return md;
113        }
114        // TODO: handle fences inside of <pre> or <code>
115        let mut in_code_block = false;
116        let mut new_md = String::new();
117        for line in md.lines() {
118            if let Some(line) = line.strip_prefix("    ") {
119                if in_code_block {
120                    new_md.push_str(&format!("{line}\n"));
121                } else {
122                    new_md.push_str(&format!("```\n{line}\n"));
123                    in_code_block = true;
124                }
125            } else {
126                if in_code_block {
127                    new_md.push_str("```\n");
128                    in_code_block = false;
129                }
130                new_md.push_str(&format!("{line}\n"));
131            }
132        }
133        if in_code_block {
134            new_md.push_str("```\n");
135        }
136        new_md.replace("```\n\n```\n", "\n")
137    }
138}