sphinx_ultra/
directives.rs

1use anyhow::{anyhow, Result};
2use regex::Regex;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// Directive validation module for comprehensive validation
7pub mod validation;
8
9/// Represents a parsed Sphinx directive
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Directive {
12    pub name: String,
13    pub arguments: Vec<String>,
14    pub options: HashMap<String, String>,
15    pub content: Vec<String>,
16    pub line_number: usize,
17    pub source_file: String,
18}
19
20/// Directive processor trait
21pub trait DirectiveProcessor {
22    fn process(&self, directive: &Directive) -> Result<String>;
23    fn get_name(&self) -> &str;
24    fn get_option_spec(&self) -> HashMap<String, DirectiveOptionType>;
25}
26
27/// Directive option types
28#[derive(Debug, Clone)]
29pub enum DirectiveOptionType {
30    Flag,
31    String,
32    Integer,
33    Float,
34    Choice(Vec<String>),
35    Unchanged,
36    UnchangedRequired,
37    Path,
38    Percentage,
39    LengthOrPercentage,
40    Class,
41    ClassOption,
42    Encoding,
43}
44
45/// Built-in directive processors
46pub struct DirectiveRegistry {
47    processors: HashMap<String, Box<dyn DirectiveProcessor + Send + Sync>>,
48}
49
50impl Default for DirectiveRegistry {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl DirectiveRegistry {
57    pub fn new() -> Self {
58        let mut registry = Self {
59            processors: HashMap::new(),
60        };
61
62        // Register built-in directives
63        registry.register_builtin_directives();
64        registry
65    }
66
67    pub fn register(&mut self, processor: Box<dyn DirectiveProcessor + Send + Sync>) {
68        self.processors
69            .insert(processor.get_name().to_string(), processor);
70    }
71
72    pub fn get(&self, name: &str) -> Option<&(dyn DirectiveProcessor + Send + Sync)> {
73        self.processors.get(name).map(|boxed| boxed.as_ref())
74    }
75
76    pub fn process_directive(&self, directive: &Directive) -> Result<String> {
77        if let Some(processor) = self.get(&directive.name) {
78            processor.process(directive)
79        } else {
80            // Return a warning comment for unknown directives
81            Ok(format!("<!-- Unknown directive: {} -->", directive.name))
82        }
83    }
84
85    fn register_builtin_directives(&mut self) {
86        // Admonition directives
87        self.register(Box::new(AdmonitionDirective::new("note")));
88        self.register(Box::new(AdmonitionDirective::new("warning")));
89        self.register(Box::new(AdmonitionDirective::new("important")));
90        self.register(Box::new(AdmonitionDirective::new("tip")));
91        self.register(Box::new(AdmonitionDirective::new("caution")));
92        self.register(Box::new(AdmonitionDirective::new("danger")));
93        self.register(Box::new(AdmonitionDirective::new("error")));
94        self.register(Box::new(AdmonitionDirective::new("hint")));
95        self.register(Box::new(AdmonitionDirective::new("attention")));
96        self.register(Box::new(AdmonitionDirective::new("seealso")));
97        self.register(Box::new(GenericAdmonitionDirective));
98
99        // Code directives
100        self.register(Box::new(CodeBlockDirective));
101        self.register(Box::new(LiteralIncludeDirective));
102        self.register(Box::new(HighlightDirective));
103
104        // Structure directives
105        self.register(Box::new(ToctreeDirective));
106        self.register(Box::new(IndexDirective));
107        self.register(Box::new(OnlyDirective));
108        self.register(Box::new(IfConfigDirective));
109
110        // Image directives
111        self.register(Box::new(ImageDirective));
112        self.register(Box::new(FigureDirective));
113
114        // Table directives
115        self.register(Box::new(TableDirective));
116        self.register(Box::new(CsvTableDirective));
117        self.register(Box::new(ListTableDirective));
118
119        // Include directives
120        self.register(Box::new(IncludeDirective));
121        self.register(Box::new(RawDirective));
122
123        // Math directives
124        self.register(Box::new(MathDirective));
125
126        // Domain-specific directives
127        self.register(Box::new(AutoDocDirective));
128        self.register(Box::new(AutoModuleDirective));
129        self.register(Box::new(AutoClassDirective));
130        self.register(Box::new(AutoFunctionDirective));
131
132        // Meta directives
133        self.register(Box::new(MetaDirective));
134        self.register(Box::new(SidebarDirective));
135        self.register(Box::new(TopicDirective));
136        self.register(Box::new(RubricDirective));
137        self.register(Box::new(EpigraphDirective));
138        self.register(Box::new(HighlightsDirective));
139        self.register(Box::new(PullQuoteDirective));
140        self.register(Box::new(CompoundDirective));
141        self.register(Box::new(ContainerDirective));
142
143        // Version directives
144        self.register(Box::new(VersionAddedDirective));
145        self.register(Box::new(VersionChangedDirective));
146        self.register(Box::new(DeprecatedDirective));
147    }
148}
149
150/// Parse a directive from RST text
151pub fn parse_directive(
152    text: &str,
153    line_number: usize,
154    source_file: &str,
155) -> Result<Option<Directive>> {
156    let directive_regex = Regex::new(r"^\.\. ([a-zA-Z][a-zA-Z0-9_-]*)::\s*(.*?)$")?;
157
158    if let Some(captures) = directive_regex.captures(text) {
159        let name = captures.get(1).unwrap().as_str().to_string();
160        let args_str = captures.get(2).unwrap().as_str();
161
162        // Parse arguments (simple space-separated for now)
163        let arguments: Vec<String> = if args_str.is_empty() {
164            Vec::new()
165        } else {
166            args_str.split_whitespace().map(|s| s.to_string()).collect()
167        };
168
169        Ok(Some(Directive {
170            name,
171            arguments,
172            options: HashMap::new(),
173            content: Vec::new(),
174            line_number,
175            source_file: source_file.to_string(),
176        }))
177    } else {
178        Ok(None)
179    }
180}
181
182// Admonition Directive
183struct AdmonitionDirective {
184    name: String,
185}
186
187impl AdmonitionDirective {
188    fn new(name: &str) -> Self {
189        Self {
190            name: name.to_string(),
191        }
192    }
193}
194
195impl DirectiveProcessor for AdmonitionDirective {
196    fn process(&self, directive: &Directive) -> Result<String> {
197        let class = if self.name == "seealso" {
198            "seealso"
199        } else {
200            &self.name
201        };
202        let title = if directive.arguments.is_empty() {
203            match self.name.as_str() {
204                "note" => "Note",
205                "warning" => "Warning",
206                "important" => "Important",
207                "tip" => "Tip",
208                "caution" => "Caution",
209                "danger" => "Danger",
210                "error" => "Error",
211                "hint" => "Hint",
212                "attention" => "Attention",
213                "seealso" => "See also",
214                _ => &self.name,
215            }
216        } else {
217            &directive.arguments[0]
218        };
219
220        let content = directive.content.join("\n");
221
222        Ok(format!(
223            "<div class=\"admonition {}\"><p class=\"admonition-title\">{}</p>{}</div>",
224            class, title, content
225        ))
226    }
227
228    fn get_name(&self) -> &str {
229        &self.name
230    }
231
232    fn get_option_spec(&self) -> HashMap<String, DirectiveOptionType> {
233        let mut options = HashMap::new();
234        options.insert("class".to_string(), DirectiveOptionType::ClassOption);
235        options.insert("name".to_string(), DirectiveOptionType::String);
236        options
237    }
238}
239
240// Generic Admonition Directive
241struct GenericAdmonitionDirective;
242
243impl DirectiveProcessor for GenericAdmonitionDirective {
244    fn process(&self, directive: &Directive) -> Result<String> {
245        let default_title = "Admonition".to_string();
246        let title = directive.arguments.first().unwrap_or(&default_title);
247        let content = directive.content.join("\n");
248
249        Ok(format!(
250            "<div class=\"admonition admonition-generic\"><p class=\"admonition-title\">{}</p>{}</div>",
251            title, content
252        ))
253    }
254
255    fn get_name(&self) -> &str {
256        "admonition"
257    }
258
259    fn get_option_spec(&self) -> HashMap<String, DirectiveOptionType> {
260        let mut options = HashMap::new();
261        options.insert("class".to_string(), DirectiveOptionType::ClassOption);
262        options.insert("name".to_string(), DirectiveOptionType::String);
263        options
264    }
265}
266
267// Code Block Directive
268struct CodeBlockDirective;
269
270impl DirectiveProcessor for CodeBlockDirective {
271    fn process(&self, directive: &Directive) -> Result<String> {
272        let default_language = "text".to_string();
273        let language = directive.arguments.first().unwrap_or(&default_language);
274        let _linenos = directive.options.contains_key("linenos");
275        let _emphasize_lines = directive.options.get("emphasize-lines");
276        let caption = directive.options.get("caption");
277        let _name = directive.options.get("name");
278
279        let content = directive.content.join("\n");
280
281        let mut html = String::new();
282
283        if let Some(caption_text) = caption {
284            html.push_str(&format!(
285                "<div class=\"code-block-caption\">{}</div>",
286                caption_text
287            ));
288        }
289
290        html.push_str(&format!(
291            "<div class=\"highlight-{}\"><pre><code class=\"language-{}\">{}</code></pre></div>",
292            language,
293            language,
294            html_escape::encode_text(&content)
295        ));
296
297        Ok(html)
298    }
299
300    fn get_name(&self) -> &str {
301        "code-block"
302    }
303
304    fn get_option_spec(&self) -> HashMap<String, DirectiveOptionType> {
305        let mut options = HashMap::new();
306        options.insert("linenos".to_string(), DirectiveOptionType::Flag);
307        options.insert("lineno-start".to_string(), DirectiveOptionType::Integer);
308        options.insert("emphasize-lines".to_string(), DirectiveOptionType::String);
309        options.insert("caption".to_string(), DirectiveOptionType::String);
310        options.insert("name".to_string(), DirectiveOptionType::String);
311        options.insert("dedent".to_string(), DirectiveOptionType::Integer);
312        options.insert("force".to_string(), DirectiveOptionType::Flag);
313        options
314    }
315}
316
317// Literal Include Directive
318struct LiteralIncludeDirective;
319
320impl DirectiveProcessor for LiteralIncludeDirective {
321    fn process(&self, directive: &Directive) -> Result<String> {
322        let filename = directive
323            .arguments
324            .first()
325            .ok_or_else(|| anyhow!("literalinclude directive requires a filename"))?;
326
327        let language = directive
328            .options
329            .get("language")
330            .cloned()
331            .or_else(|| {
332                std::path::Path::new(filename)
333                    .extension()
334                    .and_then(|ext| ext.to_str())
335                    .map(|ext| {
336                        match ext {
337                            "py" => "python",
338                            "rs" => "rust",
339                            "js" => "javascript",
340                            "ts" => "typescript",
341                            "cpp" | "cc" | "cxx" => "cpp",
342                            "c" => "c",
343                            "h" | "hpp" => "cpp",
344                            "java" => "java",
345                            "go" => "go",
346                            "php" => "php",
347                            "rb" => "ruby",
348                            "sh" | "bash" => "bash",
349                            "ps1" => "powershell",
350                            "sql" => "sql",
351                            "xml" => "xml",
352                            "html" => "html",
353                            "css" => "css",
354                            "json" => "json",
355                            "yaml" | "yml" => "yaml",
356                            "toml" => "toml",
357                            "ini" => "ini",
358                            "md" => "markdown",
359                            "rst" => "rst",
360                            "tex" => "latex",
361                            _ => "text",
362                        }
363                        .to_string()
364                    })
365            })
366            .unwrap_or_else(|| "text".to_string());
367
368        // For now, return a placeholder. In a full implementation,
369        // you would read the file and include its contents
370        Ok(format!(
371            "<div class=\"literal-include\"><div class=\"highlight-{}\"><pre><code class=\"language-{}\"><!-- Content of {} would be included here --></code></pre></div></div>",
372            language, language, filename
373        ))
374    }
375
376    fn get_name(&self) -> &str {
377        "literalinclude"
378    }
379
380    fn get_option_spec(&self) -> HashMap<String, DirectiveOptionType> {
381        let mut options = HashMap::new();
382        options.insert("language".to_string(), DirectiveOptionType::String);
383        options.insert("linenos".to_string(), DirectiveOptionType::Flag);
384        options.insert("lineno-start".to_string(), DirectiveOptionType::Integer);
385        options.insert("emphasize-lines".to_string(), DirectiveOptionType::String);
386        options.insert("lines".to_string(), DirectiveOptionType::String);
387        options.insert("start-line".to_string(), DirectiveOptionType::Integer);
388        options.insert("end-line".to_string(), DirectiveOptionType::Integer);
389        options.insert("start-after".to_string(), DirectiveOptionType::String);
390        options.insert("end-before".to_string(), DirectiveOptionType::String);
391        options.insert("prepend".to_string(), DirectiveOptionType::String);
392        options.insert("append".to_string(), DirectiveOptionType::String);
393        options.insert("dedent".to_string(), DirectiveOptionType::Integer);
394        options.insert("tab-width".to_string(), DirectiveOptionType::Integer);
395        options.insert("encoding".to_string(), DirectiveOptionType::Encoding);
396        options.insert("pyobject".to_string(), DirectiveOptionType::String);
397        options.insert("caption".to_string(), DirectiveOptionType::String);
398        options.insert("name".to_string(), DirectiveOptionType::String);
399        options.insert("class".to_string(), DirectiveOptionType::ClassOption);
400        options.insert("diff".to_string(), DirectiveOptionType::String);
401        options
402    }
403}
404
405// Highlight Directive
406struct HighlightDirective;
407
408impl DirectiveProcessor for HighlightDirective {
409    fn process(&self, directive: &Directive) -> Result<String> {
410        let default_language = "text".to_string();
411        let language = directive.arguments.first().unwrap_or(&default_language);
412        // This directive sets the highlighting language for subsequent code blocks
413        Ok(format!("<!-- highlight language set to {} -->", language))
414    }
415
416    fn get_name(&self) -> &str {
417        "highlight"
418    }
419
420    fn get_option_spec(&self) -> HashMap<String, DirectiveOptionType> {
421        let mut options = HashMap::new();
422        options.insert("linenothreshold".to_string(), DirectiveOptionType::Integer);
423        options.insert("force".to_string(), DirectiveOptionType::Flag);
424        options
425    }
426}
427
428// Additional directive implementations would go here...
429// For brevity, I'll provide stub implementations for the remaining directives
430
431macro_rules! stub_directive {
432    ($name:ident, $directive_name:expr) => {
433        struct $name;
434
435        impl DirectiveProcessor for $name {
436            fn process(&self, directive: &Directive) -> Result<String> {
437                Ok(format!(
438                    "<!-- {} directive: {} -->",
439                    $directive_name,
440                    directive.arguments.join(" ")
441                ))
442            }
443
444            fn get_name(&self) -> &str {
445                $directive_name
446            }
447
448            fn get_option_spec(&self) -> HashMap<String, DirectiveOptionType> {
449                HashMap::new()
450            }
451        }
452    };
453}
454
455stub_directive!(ToctreeDirective, "toctree");
456stub_directive!(IndexDirective, "index");
457stub_directive!(OnlyDirective, "only");
458stub_directive!(IfConfigDirective, "ifconfig");
459stub_directive!(ImageDirective, "image");
460stub_directive!(FigureDirective, "figure");
461stub_directive!(TableDirective, "table");
462stub_directive!(CsvTableDirective, "csv-table");
463stub_directive!(ListTableDirective, "list-table");
464stub_directive!(IncludeDirective, "include");
465stub_directive!(RawDirective, "raw");
466stub_directive!(MathDirective, "math");
467stub_directive!(AutoDocDirective, "autodoc");
468stub_directive!(AutoModuleDirective, "automodule");
469stub_directive!(AutoClassDirective, "autoclass");
470stub_directive!(AutoFunctionDirective, "autofunction");
471stub_directive!(MetaDirective, "meta");
472stub_directive!(SidebarDirective, "sidebar");
473stub_directive!(TopicDirective, "topic");
474stub_directive!(RubricDirective, "rubric");
475stub_directive!(EpigraphDirective, "epigraph");
476stub_directive!(HighlightsDirective, "highlights");
477stub_directive!(PullQuoteDirective, "pull-quote");
478stub_directive!(CompoundDirective, "compound");
479stub_directive!(ContainerDirective, "container");
480stub_directive!(VersionAddedDirective, "versionadded");
481stub_directive!(VersionChangedDirective, "versionchanged");
482stub_directive!(DeprecatedDirective, "deprecated");