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