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;
6use std::collections::HashMap;
7use xx::regex;
8
9#[derive(Debug, Clone)]
10pub struct MarkdownRenderer {
11    pub(crate) spec: Spec,
12    pub(crate) header_level: usize,
13    pub(crate) multi: bool,
14    tera_ctx: tera::Context,
15    url_prefix: Option<String>,
16    html_encode: bool,
17    replace_pre_with_code_fences: bool,
18}
19
20impl MarkdownRenderer {
21    pub fn new(spec: crate::Spec) -> Self {
22        let mut renderer = Self {
23            spec: spec.into(),
24            header_level: 1,
25            multi: false,
26            tera_ctx: tera::Context::new(),
27            url_prefix: None,
28            html_encode: true,
29            replace_pre_with_code_fences: false,
30        };
31        let mut spec = renderer.spec.clone();
32        spec.render_md(&renderer);
33        renderer.spec = spec;
34        renderer
35    }
36
37    pub fn with_header_level(mut self, header_level: usize) -> Self {
38        self.header_level = header_level;
39        self
40    }
41
42    pub fn with_multi(mut self, index: bool) -> Self {
43        self.multi = index;
44        self
45    }
46
47    pub fn with_url_prefix<S: Into<String>>(mut self, url_prefix: S) -> Self {
48        self.url_prefix = Some(url_prefix.into());
49        self
50    }
51
52    pub fn with_html_encode(mut self, html_encode: bool) -> Self {
53        self.html_encode = html_encode;
54        self
55    }
56
57    pub fn with_replace_pre_with_code_fences(mut self, replace_pre_with_code_fences: bool) -> Self {
58        self.replace_pre_with_code_fences = replace_pre_with_code_fences;
59        self
60    }
61
62    pub(crate) fn insert<T: Serialize + ?Sized, S: Into<String>>(&mut self, key: S, val: &T) {
63        self.tera_ctx.insert(key, val);
64    }
65
66    fn tera_ctx(&self) -> tera::Context {
67        let mut ctx = self.tera_ctx.clone();
68        ctx.insert("spec", &self.spec);
69        ctx.insert("header_level", &self.header_level);
70        ctx.insert("multi", &self.multi);
71        ctx.insert("url_prefix", &self.url_prefix);
72        ctx.insert("html_encode", &self.html_encode);
73        ctx
74    }
75
76    pub(crate) fn render(&self, template_name: &str) -> Result<String, UsageErr> {
77        let mut tera = TERA.clone();
78
79        let html_encode = self.html_encode;
80        tera.register_filter(
81            "escape_md",
82            move |value: &tera::Value, _: &HashMap<String, tera::Value>| {
83                let value = value.as_str().unwrap();
84                let value = value
85                    .lines()
86                    .map(|line| {
87                        if !html_encode || line.starts_with("    ") {
88                            return line.to_string();
89                        }
90                        // replace '<' with '&lt;' but not inside code blocks
91                        xx::regex!(r"(`[^`]*`)|(<)")
92                            .replace_all(line, |caps: &regex::Captures| {
93                                if caps.get(1).is_some() {
94                                    caps.get(1).unwrap().as_str().to_string()
95                                } else {
96                                    "&lt;".to_string()
97                                }
98                            })
99                            .to_string()
100                    })
101                    .join("\n");
102                Ok(value.into())
103            },
104        );
105        let path_re =
106            regex!(r"https://(github.com/[^/]+/[^/]+|gitlab.com/[^/]+/[^/]+/-)/blob/[^/]+/");
107        tera.register_function("source_code_link", |args: &HashMap<String, tera::Value>| {
108            let spec = args.get("spec").unwrap().as_object().unwrap();
109            let cmd = args.get("cmd").unwrap().as_object().unwrap();
110            let full_cmd = cmd.get("full_cmd").unwrap().as_array();
111            let source_code_link_template = spec
112                .get("source_code_link_template")
113                .and_then(|v| v.as_str());
114            if let (Some(full_cmd), Some(source_code_link_template)) =
115                (full_cmd, source_code_link_template)
116            {
117                if full_cmd.is_empty() {
118                    return Ok("".into());
119                }
120                let mut ctx = tera::Context::new();
121                let path = full_cmd.iter().map(|v| v.as_str().unwrap()).join("/");
122                ctx.insert("spec", spec);
123                ctx.insert("cmd", cmd);
124                ctx.insert("path", &path);
125                let href = TERA.clone().render_str(source_code_link_template, &ctx)?;
126                let friendly = path_re.replace_all(&href, "").to_string();
127                let link = if path_re.is_match(&href) {
128                    format!("[`{friendly}`]({href})")
129                } else {
130                    format!("[{friendly}]({href})")
131                };
132                Ok(link.into())
133            } else {
134                Ok("".into())
135            }
136        });
137
138        Ok(tera.render(template_name, &self.tera_ctx())?)
139    }
140
141    pub(crate) fn replace_code_fences(&self, md: String) -> String {
142        if !self.replace_pre_with_code_fences {
143            return md;
144        }
145        // TODO: handle fences inside of <pre> or <code>
146        let mut in_code_block = false;
147        let mut new_md = String::new();
148        for line in md.lines() {
149            if let Some(line) = line.strip_prefix("    ") {
150                if in_code_block {
151                    new_md.push_str(&format!("{}\n", line));
152                } else {
153                    new_md.push_str(&format!("```\n{}\n", line));
154                    in_code_block = true;
155                }
156            } else {
157                if in_code_block {
158                    new_md.push_str("```\n");
159                    in_code_block = false;
160                }
161                new_md.push_str(&format!("{}\n", line));
162            }
163        }
164        if in_code_block {
165            new_md.push_str("```\n");
166        }
167        new_md.replace("```\n\n```\n", "\n")
168    }
169}