ndg_commonmark/processor/
types.rs1use std::collections::{HashMap, HashSet};
24
25use comrak::nodes::AstNode;
26
27#[derive(Debug, Clone)]
29#[allow(
30 clippy::struct_excessive_bools,
31 reason = "Config struct with related boolean flags"
32)]
33pub struct MarkdownOptions {
34 pub gfm: bool,
36
37 pub nixpkgs: bool,
39
40 pub highlight_code: bool,
42
43 pub highlight_theme: Option<String>,
45
46 pub manpage_urls_path: Option<String>,
48
49 pub auto_link_options: bool,
53
54 pub valid_options: Option<HashSet<String>>,
58
59 pub tab_style: TabStyle,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum TabStyle {
66 None,
68 Warn,
70 Normalize,
72}
73
74impl MarkdownOptions {
75 #[must_use]
77 pub const fn with_all_features() -> Self {
78 Self {
79 gfm: cfg!(feature = "gfm"),
80 nixpkgs: cfg!(feature = "nixpkgs"),
81 highlight_code: cfg!(any(feature = "syntastica", feature = "syntect")),
82 highlight_theme: None,
83 manpage_urls_path: None,
84 auto_link_options: true,
85 valid_options: None,
86 tab_style: TabStyle::None,
87 }
88 }
89
90 #[must_use]
92 pub const fn with_features(
93 gfm: bool,
94 nixpkgs: bool,
95 highlight_code: bool,
96 ) -> Self {
97 Self {
98 gfm,
99 nixpkgs,
100 highlight_code,
101 highlight_theme: None,
102 manpage_urls_path: None,
103 auto_link_options: true,
104 valid_options: None,
105 tab_style: TabStyle::None,
106 }
107 }
108}
109
110impl Default for MarkdownOptions {
111 fn default() -> Self {
112 Self {
113 gfm: cfg!(feature = "gfm"),
114 nixpkgs: cfg!(feature = "nixpkgs"),
115 highlight_code: cfg!(feature = "syntastica"),
116 manpage_urls_path: None,
117 highlight_theme: None,
118 auto_link_options: true,
119 valid_options: None,
120 tab_style: TabStyle::None,
121 }
122 }
123}
124
125#[derive(Clone)]
129pub struct MarkdownProcessor {
130 pub(crate) options: MarkdownOptions,
131 pub(crate) manpage_urls: Option<HashMap<String, String>>,
132 pub(crate) syntax_manager: Option<crate::syntax::SyntaxManager>,
133 pub(crate) base_dir: std::path::PathBuf,
134}
135
136pub trait AstTransformer {
138 fn transform<'a>(&self, node: &'a AstNode<'a>);
139}
140
141pub struct PromptTransformer;
144
145impl AstTransformer for PromptTransformer {
146 fn transform<'a>(&self, node: &'a AstNode<'a>) {
147 use std::sync::LazyLock;
148
149 use comrak::nodes::NodeValue;
150 use regex::Regex;
151
152 static COMMAND_PROMPT_RE: LazyLock<Regex> = LazyLock::new(|| {
153 Regex::new(r"^\s*\$\s+(.+)$").unwrap_or_else(|e| {
154 log::error!(
155 "Failed to compile COMMAND_PROMPT_RE regex: {e}\n Falling back to \
156 never matching regex."
157 );
158 crate::utils::never_matching_regex().unwrap_or_else(|_| {
159 #[allow(
161 clippy::expect_used,
162 reason = "This pattern is guaranteed to be valid"
163 )]
164 Regex::new(r"[^\s\S]")
165 .expect("regex pattern [^\\s\\S] should always compile")
166 })
167 })
168 });
169 static REPL_PROMPT_RE: LazyLock<Regex> = LazyLock::new(|| {
170 Regex::new(r"^nix-repl>\s*(.*)$").unwrap_or_else(|e| {
171 log::error!(
172 "Failed to compile REPL_PROMPT_RE regex: {e}\n Falling back to \
173 never matching regex."
174 );
175 crate::utils::never_matching_regex().unwrap_or_else(|_| {
176 #[allow(
178 clippy::expect_used,
179 reason = "This pattern is guaranteed to be valid"
180 )]
181 Regex::new(r"[^\s\S]")
182 .expect("regex pattern [^\\s\\S] should always compile")
183 })
184 })
185 });
186
187 for child in node.children() {
188 {
189 let mut data = child.data.borrow_mut();
190 if let NodeValue::Code(ref code) = data.value {
191 let literal = code.literal.trim();
192
193 if let Some(caps) = COMMAND_PROMPT_RE.captures(literal) {
195 if !literal.starts_with("\\$") && !literal.starts_with("$$") {
197 let command = caps[1].trim();
198 let html = format!(
199 "<code class=\"terminal\"><span class=\"prompt\">$</span> \
200 {command}</code>"
201 );
202 data.value = NodeValue::HtmlInline(html);
203 }
204 } else if let Some(caps) = REPL_PROMPT_RE.captures(literal) {
205 if !literal.starts_with("nix-repl>>") {
207 let expression = caps[1].trim();
208 let html = format!(
209 "<code class=\"nix-repl\"><span \
210 class=\"prompt\">nix-repl></span> {expression}</code>"
211 );
212 data.value = NodeValue::HtmlInline(html);
213 }
214 }
215 }
216 }
217 self.transform(child);
218 }
219 }
220}
221
222#[derive(Debug, Clone)]
224pub struct MarkdownOptionsBuilder {
225 options: MarkdownOptions,
226}
227
228impl MarkdownOptionsBuilder {
229 #[must_use]
231 pub fn new() -> Self {
232 Self {
233 options: MarkdownOptions::default(),
234 }
235 }
236
237 #[must_use]
239 pub const fn gfm(mut self, enabled: bool) -> Self {
240 self.options.gfm = enabled;
241 self
242 }
243
244 #[must_use]
246 pub const fn nixpkgs(mut self, enabled: bool) -> Self {
247 self.options.nixpkgs = enabled;
248 self
249 }
250
251 #[must_use]
253 pub const fn highlight_code(mut self, enabled: bool) -> Self {
254 self.options.highlight_code = enabled;
255 self
256 }
257
258 #[must_use]
260 pub fn highlight_theme<S: Into<String>>(mut self, theme: Option<S>) -> Self {
261 self.options.highlight_theme = theme.map(Into::into);
262 self
263 }
264
265 #[must_use]
267 pub fn manpage_urls_path<S: Into<String>>(mut self, path: Option<S>) -> Self {
268 self.options.manpage_urls_path = path.map(Into::into);
269 self
270 }
271
272 #[must_use]
274 pub const fn auto_link_options(mut self, enabled: bool) -> Self {
275 self.options.auto_link_options = enabled;
276 self
277 }
278
279 #[must_use]
281 pub fn valid_options(mut self, options: Option<HashSet<String>>) -> Self {
282 self.options.valid_options = options;
283 self
284 }
285
286 #[must_use]
288 pub const fn tab_style(mut self, style: TabStyle) -> Self {
289 self.options.tab_style = style;
290 self
291 }
292
293 #[must_use]
295 pub fn build(self) -> MarkdownOptions {
296 self.options
297 }
298
299 #[must_use]
301 pub fn from_external_config<T>(_config: &T) -> Self {
302 Self::new()
303 }
304}
305
306impl Default for MarkdownOptionsBuilder {
307 fn default() -> Self {
308 Self::new()
309 }
310}