1use anyhow::{anyhow, Result};
2use regex::Regex;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6pub mod validation;
8
9#[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
20pub 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#[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
45pub 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 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 Ok(format!("<!-- Unknown directive: {} -->", directive.name))
82 }
83 }
84
85 fn register_builtin_directives(&mut self) {
86 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 self.register(Box::new(CodeBlockDirective));
101 self.register(Box::new(LiteralIncludeDirective));
102 self.register(Box::new(HighlightDirective));
103
104 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 self.register(Box::new(ImageDirective));
112 self.register(Box::new(FigureDirective));
113
114 self.register(Box::new(TableDirective));
116 self.register(Box::new(CsvTableDirective));
117 self.register(Box::new(ListTableDirective));
118
119 self.register(Box::new(IncludeDirective));
121 self.register(Box::new(RawDirective));
122
123 self.register(Box::new(MathDirective));
125
126 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 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 self.register(Box::new(VersionAddedDirective));
145 self.register(Box::new(VersionChangedDirective));
146 self.register(Box::new(DeprecatedDirective));
147 }
148}
149
150pub 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 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
182struct 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
240struct 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
267struct 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
317struct 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 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
405struct 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 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
428macro_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");