ndg_commonmark/processor/
types.rs1use std::sync::LazyLock;
24
25use comrak::nodes::AstNode;
26use regex::Regex;
27use rustc_hash::{FxHashMap, FxHashSet};
28
29static COMMAND_PROMPT_RE: LazyLock<Regex> = LazyLock::new(|| {
30 Regex::new(r"^\s*\$\s+(.+)$").unwrap_or_else(|e| {
31 log::error!(
32 "Failed to compile COMMAND_PROMPT_RE regex: {e}\n Falling back to never \
33 matching regex."
34 );
35 crate::utils::never_matching_regex().unwrap_or_else(|_| {
36 #[expect(
37 clippy::expect_used,
38 reason = "This pattern is guaranteed to be valid"
39 )]
40 Regex::new(r"[^\s\S]")
41 .expect("regex pattern [^\\s\\S] should always compile")
42 })
43 })
44});
45
46static REPL_PROMPT_RE: LazyLock<Regex> = LazyLock::new(|| {
47 Regex::new(r"^nix-repl>\s*(.*)$").unwrap_or_else(|e| {
48 log::error!(
49 "Failed to compile REPL_PROMPT_RE regex: {e}\n Falling back to never \
50 matching regex."
51 );
52 crate::utils::never_matching_regex().unwrap_or_else(|_| {
53 #[expect(
54 clippy::expect_used,
55 reason = "This pattern is guaranteed to be valid"
56 )]
57 Regex::new(r"[^\s\S]")
58 .expect("regex pattern [^\\s\\S] should always compile")
59 })
60 })
61});
62
63#[derive(Debug, Clone)]
65#[expect(
66 clippy::struct_excessive_bools,
67 reason = "Config struct with related boolean flags"
68)]
69pub struct MarkdownOptions {
70 pub gfm: bool,
72
73 pub nixpkgs: bool,
75
76 pub highlight_code: bool,
78
79 pub highlight_theme: Option<String>,
81
82 pub manpage_urls_path: Option<String>,
84
85 pub syntax_queries_path: Option<String>,
90
91 pub auto_link_options: bool,
95
96 pub valid_options: Option<FxHashSet<String>>,
100
101 pub tab_style: TabStyle,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum TabStyle {
108 None,
110 Warn,
112 Normalize,
114}
115
116impl MarkdownOptions {
117 #[must_use]
119 pub const fn with_all_features() -> Self {
120 Self {
121 gfm: cfg!(feature = "gfm"),
122 nixpkgs: cfg!(feature = "nixpkgs"),
123 highlight_code: cfg!(any(
124 feature = "syntastica",
125 feature = "syntect"
126 )),
127 highlight_theme: None,
128 manpage_urls_path: None,
129 syntax_queries_path: None,
130 auto_link_options: true,
131 valid_options: None,
132 tab_style: TabStyle::None,
133 }
134 }
135
136 #[must_use]
138 pub const fn with_features(
139 gfm: bool,
140 nixpkgs: bool,
141 highlight_code: bool,
142 ) -> Self {
143 Self {
144 gfm,
145 nixpkgs,
146 highlight_code,
147 highlight_theme: None,
148 manpage_urls_path: None,
149 syntax_queries_path: None,
150 auto_link_options: true,
151 valid_options: None,
152 tab_style: TabStyle::None,
153 }
154 }
155}
156
157impl Default for MarkdownOptions {
158 fn default() -> Self {
159 Self {
160 gfm: cfg!(feature = "gfm"),
161 nixpkgs: cfg!(feature = "nixpkgs"),
162 highlight_code: cfg!(feature = "syntastica"),
163 manpage_urls_path: None,
164 syntax_queries_path: None,
165 highlight_theme: None,
166 auto_link_options: true,
167 valid_options: None,
168 tab_style: TabStyle::None,
169 }
170 }
171}
172
173#[derive(Clone)]
177pub struct MarkdownProcessor {
178 pub(crate) options: MarkdownOptions,
179 pub(crate) manpage_urls: Option<FxHashMap<String, String>>,
180 pub(crate) syntax_manager: Option<crate::syntax::SyntaxManager>,
181 pub(crate) base_dir: std::path::PathBuf,
182}
183
184pub trait AstTransformer {
186 fn transform<'a>(&self, node: &'a AstNode<'a>);
187}
188
189pub struct PromptTransformer;
192
193impl AstTransformer for PromptTransformer {
194 fn transform<'a>(&self, node: &'a AstNode<'a>) {
195 use comrak::nodes::NodeValue;
196
197 for child in node.children() {
198 {
199 let mut data = child.data.borrow_mut();
200 if let NodeValue::Code(ref code) = data.value {
201 let literal = code.literal.trim();
202
203 if let Some(caps) = COMMAND_PROMPT_RE.captures(literal) {
205 if !literal.starts_with("\\$") && !literal.starts_with("$$") {
207 let command = caps[1].trim();
208 let html = format!(
209 "<code class=\"terminal\"><span class=\"prompt\">$</span> \
210 {command}</code>"
211 );
212 data.value = NodeValue::HtmlInline(html);
213 }
214 } else if let Some(caps) = REPL_PROMPT_RE.captures(literal) {
215 if !literal.starts_with("nix-repl>>") {
217 let expression = caps[1].trim();
218 let html = format!(
219 "<code class=\"nix-repl\"><span \
220 class=\"prompt\">nix-repl></span> {expression}</code>"
221 );
222 data.value = NodeValue::HtmlInline(html);
223 }
224 }
225 }
226 }
227 self.transform(child);
228 }
229 }
230}
231
232#[derive(Debug, Clone)]
234pub struct MarkdownOptionsBuilder {
235 options: MarkdownOptions,
236}
237
238impl MarkdownOptionsBuilder {
239 #[must_use]
241 pub fn new() -> Self {
242 Self {
243 options: MarkdownOptions::default(),
244 }
245 }
246
247 #[must_use]
249 pub const fn gfm(mut self, enabled: bool) -> Self {
250 self.options.gfm = enabled;
251 self
252 }
253
254 #[must_use]
256 pub const fn nixpkgs(mut self, enabled: bool) -> Self {
257 self.options.nixpkgs = enabled;
258 self
259 }
260
261 #[must_use]
263 pub const fn highlight_code(mut self, enabled: bool) -> Self {
264 self.options.highlight_code = enabled;
265 self
266 }
267
268 #[must_use]
270 pub fn highlight_theme<S: Into<String>>(self, theme: Option<S>) -> Self {
271 fn inner(
272 mut this: MarkdownOptionsBuilder,
273 theme: Option<String>,
274 ) -> MarkdownOptionsBuilder {
275 this.options.highlight_theme = theme;
276 this
277 }
278 inner(self, theme.map(Into::into))
279 }
280
281 #[must_use]
283 pub fn manpage_urls_path<S: Into<String>>(self, path: Option<S>) -> Self {
284 fn inner(
285 mut this: MarkdownOptionsBuilder,
286 path: Option<String>,
287 ) -> MarkdownOptionsBuilder {
288 this.options.manpage_urls_path = path;
289 this
290 }
291 inner(self, path.map(Into::into))
292 }
293
294 #[must_use]
296 pub fn syntax_queries_path<S: Into<String>>(self, path: Option<S>) -> Self {
297 fn inner(
298 mut this: MarkdownOptionsBuilder,
299 path: Option<String>,
300 ) -> MarkdownOptionsBuilder {
301 this.options.syntax_queries_path = path;
302 this
303 }
304 inner(self, path.map(Into::into))
305 }
306
307 #[must_use]
309 pub const fn auto_link_options(mut self, enabled: bool) -> Self {
310 self.options.auto_link_options = enabled;
311 self
312 }
313
314 #[must_use]
316 pub fn valid_options(mut self, options: Option<FxHashSet<String>>) -> Self {
317 self.options.valid_options = options;
318 self
319 }
320
321 #[must_use]
323 pub const fn tab_style(mut self, style: TabStyle) -> Self {
324 self.options.tab_style = style;
325 self
326 }
327
328 #[must_use]
330 pub fn build(self) -> MarkdownOptions {
331 self.options
332 }
333}
334
335impl Default for MarkdownOptionsBuilder {
336 fn default() -> Self {
337 Self::new()
338 }
339}