usage/docs/markdown/
renderer.rs1use 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 xx::regex!(r"(`[^`]*`)|(<)")
92 .replace_all(line, |caps: ®ex::Captures| {
93 if caps.get(1).is_some() {
94 caps.get(1).unwrap().as_str().to_string()
95 } else {
96 "<".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 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}