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