ndg_commonmark/processor/
types.rs1use std::{str::FromStr, sync::LazyLock};
24
25use comrak::nodes::AstNode;
26use regex::Regex;
27use rustc_hash::{FxHashMap, FxHashSet};
28use serde::{Deserialize, Serialize};
29
30static COMMAND_PROMPT_RE: LazyLock<Regex> = LazyLock::new(|| {
31 Regex::new(r"^\s*\$\s+(.+)$").unwrap_or_else(|e| {
32 log::error!(
33 "Failed to compile COMMAND_PROMPT_RE regex: {e}\n Falling back to never \
34 matching regex."
35 );
36 crate::utils::never_matching_regex().unwrap_or_else(|_| {
37 #[expect(
38 clippy::expect_used,
39 reason = "This pattern is guaranteed to be valid"
40 )]
41 Regex::new(r"[^\s\S]")
42 .expect("regex pattern [^\\s\\S] should always compile")
43 })
44 })
45});
46
47static REPL_PROMPT_RE: LazyLock<Regex> = LazyLock::new(|| {
48 Regex::new(r"^nix-repl>\s*(.*)$").unwrap_or_else(|e| {
49 log::error!(
50 "Failed to compile REPL_PROMPT_RE regex: {e}\n Falling back to never \
51 matching regex."
52 );
53 crate::utils::never_matching_regex().unwrap_or_else(|_| {
54 #[expect(
55 clippy::expect_used,
56 reason = "This pattern is guaranteed to be valid"
57 )]
58 Regex::new(r"[^\s\S]")
59 .expect("regex pattern [^\\s\\S] should always compile")
60 })
61 })
62});
63
64#[derive(Debug, Clone)]
66#[expect(
67 clippy::struct_excessive_bools,
68 reason = "Config struct with related boolean flags"
69)]
70pub struct MarkdownOptions {
71 pub gfm: bool,
73
74 pub nixpkgs: bool,
76
77 pub highlight_code: bool,
79
80 pub extensions: Vec<MarkdownExtension>,
82
83 pub highlight_theme: Option<String>,
85
86 pub manpage_urls: Option<FxHashMap<String, String>>,
88
89 pub syntax_queries_path: Option<String>,
94
95 pub auto_link_options: bool,
99
100 pub valid_options: Option<FxHashSet<String>>,
104
105 pub tab_style: TabStyle,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum TabStyle {
112 None,
114 Warn,
116 Normalize,
118}
119
120impl MarkdownOptions {
121 #[must_use]
123 pub const fn with_all_features() -> Self {
124 Self {
125 gfm: cfg!(feature = "gfm"),
126 nixpkgs: cfg!(feature = "nixpkgs"),
127 highlight_code: cfg!(any(
128 feature = "syntastica",
129 feature = "syntect"
130 )),
131 extensions: Vec::new(),
132 highlight_theme: None,
133 manpage_urls: None,
134 syntax_queries_path: None,
135 auto_link_options: true,
136 valid_options: None,
137 tab_style: TabStyle::None,
138 }
139 }
140
141 #[must_use]
143 pub const fn with_features(
144 gfm: bool,
145 nixpkgs: bool,
146 highlight_code: bool,
147 ) -> Self {
148 Self {
149 gfm,
150 nixpkgs,
151 highlight_code,
152 extensions: Vec::new(),
153 highlight_theme: None,
154 manpage_urls: None,
155 syntax_queries_path: None,
156 auto_link_options: true,
157 valid_options: None,
158 tab_style: TabStyle::None,
159 }
160 }
161}
162
163impl Default for MarkdownOptions {
164 fn default() -> Self {
165 Self {
166 gfm: cfg!(feature = "gfm"),
167 nixpkgs: cfg!(feature = "nixpkgs"),
168 highlight_code: cfg!(feature = "syntastica"),
169 extensions: Vec::new(),
170 manpage_urls: None,
171 syntax_queries_path: None,
172 highlight_theme: None,
173 auto_link_options: true,
174 valid_options: None,
175 tab_style: TabStyle::None,
176 }
177 }
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
182#[serde(rename_all = "kebab-case")]
183pub enum MarkdownExtension {
184 Alerts,
186 Autolink,
188 BlockDirective,
190 CjkFriendlyEmphasis,
192 Greentext,
194 DescriptionLists,
196 Footnotes,
198 Highlight,
200 Insert,
202 InlineFootnotes,
204 MathCode,
206 MathDollars,
208 MathLatex,
210 MultilineBlockQuotes,
212 Spoiler,
214 Strikethrough,
216 Subscript,
218 Subtext,
220 Superscript,
222 Table,
224 Tasklist,
226 Underline,
228 WikilinksTitleAfterPipe,
230 WikilinksTitleBeforePipe,
232}
233
234impl FromStr for MarkdownExtension {
235 type Err = String;
236
237 fn from_str(value: &str) -> Result<Self, Self::Err> {
238 match value {
239 "alerts" => Ok(Self::Alerts),
240 "autolink" => Ok(Self::Autolink),
241 "block-directive" => Ok(Self::BlockDirective),
242 "cjk-friendly-emphasis" => Ok(Self::CjkFriendlyEmphasis),
243 "greentext" => Ok(Self::Greentext),
244 "description-lists" => Ok(Self::DescriptionLists),
245 "footnotes" => Ok(Self::Footnotes),
246 "highlight" => Ok(Self::Highlight),
247 "insert" => Ok(Self::Insert),
248 "inline-footnotes" => Ok(Self::InlineFootnotes),
249 "math-code" => Ok(Self::MathCode),
250 "math-dollars" => Ok(Self::MathDollars),
251 "math-latex" => Ok(Self::MathLatex),
252 "multiline-block-quotes" => Ok(Self::MultilineBlockQuotes),
253 "spoiler" => Ok(Self::Spoiler),
254 "strikethrough" => Ok(Self::Strikethrough),
255 "subscript" => Ok(Self::Subscript),
256 "subtext" => Ok(Self::Subtext),
257 "superscript" => Ok(Self::Superscript),
258 "table" => Ok(Self::Table),
259 "tasklist" => Ok(Self::Tasklist),
260 "underline" => Ok(Self::Underline),
261 "wikilinks-title-after-pipe" => Ok(Self::WikilinksTitleAfterPipe),
262 "wikilinks-title-before-pipe" => Ok(Self::WikilinksTitleBeforePipe),
263 _ => Err(format!("unknown Markdown extension: {value}")),
264 }
265 }
266}
267
268#[derive(Clone)]
272pub struct MarkdownProcessor {
273 pub(crate) options: MarkdownOptions,
274 pub(crate) manpage_urls: Option<FxHashMap<String, String>>,
275 pub(crate) syntax_manager: Option<crate::syntax::SyntaxManager>,
276 pub(crate) base_dir: std::path::PathBuf,
277}
278
279pub trait AstTransformer {
281 fn transform<'a>(&self, node: &'a AstNode<'a>);
282}
283
284pub struct PromptTransformer;
287
288impl AstTransformer for PromptTransformer {
289 fn transform<'a>(&self, node: &'a AstNode<'a>) {
290 use comrak::nodes::NodeValue;
291
292 for child in node.children() {
293 {
294 let mut data = child.data.borrow_mut();
295 if let NodeValue::Code(ref code) = data.value {
296 let literal = code.literal.trim();
297
298 if let Some(caps) = COMMAND_PROMPT_RE.captures(literal) {
300 if !literal.starts_with("\\$") && !literal.starts_with("$$") {
302 let command = caps[1].trim();
303 let html = format!(
304 "<code class=\"terminal\"><span class=\"prompt\">$</span> \
305 {command}</code>"
306 );
307 data.value = NodeValue::HtmlInline(html);
308 }
309 } else if let Some(caps) = REPL_PROMPT_RE.captures(literal) {
310 if !literal.starts_with("nix-repl>>") {
312 let expression = caps[1].trim();
313 let html = format!(
314 "<code class=\"nix-repl\"><span \
315 class=\"prompt\">nix-repl></span> {expression}</code>"
316 );
317 data.value = NodeValue::HtmlInline(html);
318 }
319 }
320 }
321 }
322 self.transform(child);
323 }
324 }
325}
326
327#[derive(Debug, Clone)]
329pub struct MarkdownOptionsBuilder {
330 options: MarkdownOptions,
331}
332
333impl MarkdownOptionsBuilder {
334 #[must_use]
336 pub fn new() -> Self {
337 Self {
338 options: MarkdownOptions::default(),
339 }
340 }
341
342 #[must_use]
344 pub const fn gfm(mut self, enabled: bool) -> Self {
345 self.options.gfm = enabled;
346 self
347 }
348
349 #[must_use]
351 pub const fn nixpkgs(mut self, enabled: bool) -> Self {
352 self.options.nixpkgs = enabled;
353 self
354 }
355
356 #[must_use]
358 pub const fn highlight_code(mut self, enabled: bool) -> Self {
359 self.options.highlight_code = enabled;
360 self
361 }
362
363 #[must_use]
365 pub fn extensions(mut self, extensions: Vec<MarkdownExtension>) -> Self {
366 self.options.extensions = extensions;
367 self
368 }
369
370 #[must_use]
372 pub fn highlight_theme<S: Into<String>>(self, theme: Option<S>) -> Self {
373 fn inner(
374 mut this: MarkdownOptionsBuilder,
375 theme: Option<String>,
376 ) -> MarkdownOptionsBuilder {
377 this.options.highlight_theme = theme;
378 this
379 }
380 inner(self, theme.map(Into::into))
381 }
382
383 #[must_use]
385 pub fn manpage_urls(
386 mut self,
387 mappings: Option<FxHashMap<String, String>>,
388 ) -> Self {
389 self.options.manpage_urls = mappings;
390 self
391 }
392
393 #[must_use]
395 pub fn syntax_queries_path<S: Into<String>>(self, path: Option<S>) -> Self {
396 fn inner(
397 mut this: MarkdownOptionsBuilder,
398 path: Option<String>,
399 ) -> MarkdownOptionsBuilder {
400 this.options.syntax_queries_path = path;
401 this
402 }
403 inner(self, path.map(Into::into))
404 }
405
406 #[must_use]
408 pub const fn auto_link_options(mut self, enabled: bool) -> Self {
409 self.options.auto_link_options = enabled;
410 self
411 }
412
413 #[must_use]
415 pub fn valid_options(mut self, options: Option<FxHashSet<String>>) -> Self {
416 self.options.valid_options = options;
417 self
418 }
419
420 #[must_use]
422 pub const fn tab_style(mut self, style: TabStyle) -> Self {
423 self.options.tab_style = style;
424 self
425 }
426
427 #[must_use]
429 pub fn build(self) -> MarkdownOptions {
430 self.options
431 }
432}
433
434impl Default for MarkdownOptionsBuilder {
435 fn default() -> Self {
436 Self::new()
437 }
438}