sphinx_ultra/directives/validation/
roles.rs1use super::{ParsedRole, RoleValidationResult, RoleValidator};
4
5#[derive(Default)]
7pub struct DocRoleValidator;
8
9impl DocRoleValidator {
10 pub fn new() -> Self {
11 Self
12 }
13}
14
15impl RoleValidator for DocRoleValidator {
16 fn name(&self) -> &str {
17 "doc"
18 }
19
20 fn validate(&self, role: &ParsedRole) -> RoleValidationResult {
21 if role.target.is_empty() {
22 return RoleValidationResult::Error("Doc role requires a document target".to_string());
23 }
24
25 if role.target.ends_with(".rst") || role.target.ends_with(".md") {
29 return RoleValidationResult::Warning(
30 "Document reference should not include file extension".to_string(),
31 );
32 }
33
34 RoleValidationResult::Valid
35 }
36
37 fn requires_target(&self) -> bool {
38 true
39 }
40
41 fn allows_display_text(&self) -> bool {
42 true
43 }
44}
45
46#[derive(Default)]
48pub struct RefRoleValidator;
49
50impl RefRoleValidator {
51 pub fn new() -> Self {
52 Self
53 }
54}
55
56impl RoleValidator for RefRoleValidator {
57 fn name(&self) -> &str {
58 "ref"
59 }
60
61 fn validate(&self, role: &ParsedRole) -> RoleValidationResult {
62 if role.target.is_empty() {
63 return RoleValidationResult::Error("Ref role requires a reference target".to_string());
64 }
65
66 RoleValidationResult::Valid
69 }
70
71 fn requires_target(&self) -> bool {
72 true
73 }
74
75 fn allows_display_text(&self) -> bool {
76 true
77 }
78}
79
80#[derive(Default)]
82pub struct DownloadRoleValidator;
83
84impl DownloadRoleValidator {
85 pub fn new() -> Self {
86 Self
87 }
88}
89
90impl RoleValidator for DownloadRoleValidator {
91 fn name(&self) -> &str {
92 "download"
93 }
94
95 fn validate(&self, role: &ParsedRole) -> RoleValidationResult {
96 if role.target.is_empty() {
97 return RoleValidationResult::Error("Download role requires a file path".to_string());
98 }
99
100 let downloadable_extensions = [
102 "pdf", "zip", "tar", "gz", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "csv",
103 "json", "xml", "sql", "py", "rs", "js", "cpp", "c", "h", "java", "go", "rb", "php",
104 ];
105
106 if let Some(extension) = role.target.split('.').next_back() {
107 if !downloadable_extensions.contains(&extension.to_lowercase().as_str()) {
108 return RoleValidationResult::Warning(format!(
109 "Unusual file type for download: {}",
110 extension
111 ));
112 }
113 } else {
114 return RoleValidationResult::Warning(
115 "Download target has no file extension".to_string(),
116 );
117 }
118
119 if role.target.starts_with("http://") || role.target.starts_with("https://") {
121 return RoleValidationResult::Warning(
122 "Download role should reference local files, not URLs".to_string(),
123 );
124 }
125
126 RoleValidationResult::Valid
127 }
128
129 fn requires_target(&self) -> bool {
130 true
131 }
132
133 fn allows_display_text(&self) -> bool {
134 true
135 }
136}
137
138#[derive(Default)]
140pub struct MathRoleValidator;
141
142impl MathRoleValidator {
143 pub fn new() -> Self {
144 Self
145 }
146}
147
148impl RoleValidator for MathRoleValidator {
149 fn name(&self) -> &str {
150 "math"
151 }
152
153 fn validate(&self, role: &ParsedRole) -> RoleValidationResult {
154 if role.target.is_empty() {
155 return RoleValidationResult::Error(
156 "Math role requires LaTeX math expression".to_string(),
157 );
158 }
159
160 let open_braces = role.target.matches('{').count();
162 let close_braces = role.target.matches('}').count();
163
164 if open_braces != close_braces {
165 return RoleValidationResult::Warning(
166 "Unmatched braces in math expression".to_string(),
167 );
168 }
169
170 if role.target.contains('\\')
172 && !role.target.contains("\\frac")
173 && !role.target.contains("\\sqrt")
174 {
175 }
177
178 RoleValidationResult::Valid
179 }
180
181 fn requires_target(&self) -> bool {
182 true
183 }
184
185 fn allows_display_text(&self) -> bool {
186 false
187 }
188}
189
190#[derive(Default)]
192pub struct AbbreviationRoleValidator;
193
194impl AbbreviationRoleValidator {
195 pub fn new() -> Self {
196 Self
197 }
198}
199
200impl RoleValidator for AbbreviationRoleValidator {
201 fn name(&self) -> &str {
202 "abbr"
203 }
204
205 fn validate(&self, role: &ParsedRole) -> RoleValidationResult {
206 if role.target.is_empty() {
207 return RoleValidationResult::Error("Abbreviation role requires text".to_string());
208 }
209
210 if !role.target.chars().any(|c| c.is_uppercase()) {
212 return RoleValidationResult::Warning(
213 "Abbreviations typically contain uppercase letters".to_string(),
214 );
215 }
216
217 RoleValidationResult::Valid
218 }
219
220 fn requires_target(&self) -> bool {
221 true
222 }
223
224 fn allows_display_text(&self) -> bool {
225 true
226 }
227}
228
229#[derive(Default)]
231pub struct CommandRoleValidator;
232
233impl CommandRoleValidator {
234 pub fn new() -> Self {
235 Self
236 }
237}
238
239impl RoleValidator for CommandRoleValidator {
240 fn name(&self) -> &str {
241 "command"
242 }
243
244 fn validate(&self, role: &ParsedRole) -> RoleValidationResult {
245 if role.target.is_empty() {
246 return RoleValidationResult::Error("Command role requires a command name".to_string());
247 }
248
249 let dangerous_chars = ['&', '|', ';', '`', '$', '(', ')', '<', '>'];
251 if role.target.chars().any(|c| dangerous_chars.contains(&c)) {
252 return RoleValidationResult::Warning(
253 "Command contains potentially dangerous characters".to_string(),
254 );
255 }
256
257 RoleValidationResult::Valid
258 }
259
260 fn requires_target(&self) -> bool {
261 true
262 }
263
264 fn allows_display_text(&self) -> bool {
265 false
266 }
267}
268
269#[derive(Default)]
271pub struct FileRoleValidator;
272
273impl FileRoleValidator {
274 pub fn new() -> Self {
275 Self
276 }
277}
278
279impl RoleValidator for FileRoleValidator {
280 fn name(&self) -> &str {
281 "file"
282 }
283
284 fn validate(&self, role: &ParsedRole) -> RoleValidationResult {
285 if role.target.is_empty() {
286 return RoleValidationResult::Error("File role requires a file path".to_string());
287 }
288
289 let invalid_chars = ['<', '>', ':', '"', '|', '?', '*'];
291 if role.target.chars().any(|c| invalid_chars.contains(&c)) {
292 return RoleValidationResult::Error(
293 "File path contains invalid characters".to_string(),
294 );
295 }
296
297 RoleValidationResult::Valid
298 }
299
300 fn requires_target(&self) -> bool {
301 true
302 }
303
304 fn allows_display_text(&self) -> bool {
305 false
306 }
307}
308
309#[derive(Default)]
311pub struct KbdRoleValidator;
312
313impl KbdRoleValidator {
314 pub fn new() -> Self {
315 Self
316 }
317}
318
319impl RoleValidator for KbdRoleValidator {
320 fn name(&self) -> &str {
321 "kbd"
322 }
323
324 fn validate(&self, role: &ParsedRole) -> RoleValidationResult {
325 if role.target.is_empty() {
326 return RoleValidationResult::Error("Kbd role requires key combination".to_string());
327 }
328
329 RoleValidationResult::Valid
331 }
332
333 fn requires_target(&self) -> bool {
334 true
335 }
336
337 fn allows_display_text(&self) -> bool {
338 false
339 }
340}
341
342#[derive(Default)]
344pub struct MenuSelectionRoleValidator;
345
346impl MenuSelectionRoleValidator {
347 pub fn new() -> Self {
348 Self
349 }
350}
351
352impl RoleValidator for MenuSelectionRoleValidator {
353 fn name(&self) -> &str {
354 "menuselection"
355 }
356
357 fn validate(&self, role: &ParsedRole) -> RoleValidationResult {
358 if role.target.is_empty() {
359 return RoleValidationResult::Error(
360 "Menu selection role requires menu path".to_string(),
361 );
362 }
363
364 RoleValidationResult::Valid
366 }
367
368 fn requires_target(&self) -> bool {
369 true
370 }
371
372 fn allows_display_text(&self) -> bool {
373 false
374 }
375}
376
377#[derive(Default)]
379pub struct GuiLabelRoleValidator;
380
381impl GuiLabelRoleValidator {
382 pub fn new() -> Self {
383 Self
384 }
385}
386
387impl RoleValidator for GuiLabelRoleValidator {
388 fn name(&self) -> &str {
389 "guilabel"
390 }
391
392 fn validate(&self, role: &ParsedRole) -> RoleValidationResult {
393 if role.target.is_empty() {
394 return RoleValidationResult::Error("GUI label role requires label text".to_string());
395 }
396
397 if role.target.contains('&') && !role.target.contains("&") {
399 return RoleValidationResult::Warning(
400 "Use & for literal ampersand in GUI labels".to_string(),
401 );
402 }
403
404 RoleValidationResult::Valid
405 }
406
407 fn requires_target(&self) -> bool {
408 true
409 }
410
411 fn allows_display_text(&self) -> bool {
412 false
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use crate::directives::validation::SourceLocation;
420
421 fn create_test_role(name: &str, target: &str, display_text: Option<String>) -> ParsedRole {
422 ParsedRole {
423 name: name.to_string(),
424 target: target.to_string(),
425 display_text,
426 location: SourceLocation {
427 file: "test.rst".to_string(),
428 line: 1,
429 column: 1,
430 },
431 }
432 }
433
434 #[test]
435 fn test_doc_role_validator() {
436 let validator = DocRoleValidator::new();
437
438 let role = create_test_role("doc", "installation", None);
440 assert_eq!(validator.validate(&role), RoleValidationResult::Valid);
441
442 let role = create_test_role("doc", "", None);
444 assert!(matches!(
445 validator.validate(&role),
446 RoleValidationResult::Error(_)
447 ));
448
449 let role = create_test_role("doc", "installation.rst", None);
451 assert!(matches!(
452 validator.validate(&role),
453 RoleValidationResult::Warning(_)
454 ));
455 }
456
457 #[test]
458 fn test_ref_role_validator() {
459 let validator = RefRoleValidator::new();
460
461 let role = create_test_role("ref", "advanced-usage", None);
463 assert_eq!(validator.validate(&role), RoleValidationResult::Valid);
464
465 let role = create_test_role("ref", "advanced usage", None);
467 assert_eq!(validator.validate(&role), RoleValidationResult::Valid);
468
469 let role = create_test_role("ref", "Advanced-Usage", None);
471 assert_eq!(validator.validate(&role), RoleValidationResult::Valid);
472 }
473
474 #[test]
475 fn test_download_role_validator() {
476 let validator = DownloadRoleValidator::new();
477
478 let role = create_test_role("download", "example.pdf", None);
480 assert_eq!(validator.validate(&role), RoleValidationResult::Valid);
481
482 let role = create_test_role("download", "example", None);
484 assert!(matches!(
485 validator.validate(&role),
486 RoleValidationResult::Warning(_)
487 ));
488
489 let role = create_test_role("download", "https://example.com/file.pdf", None);
491 assert!(matches!(
492 validator.validate(&role),
493 RoleValidationResult::Warning(_)
494 ));
495 }
496
497 #[test]
498 fn test_math_role_validator() {
499 let validator = MathRoleValidator::new();
500
501 let role = create_test_role("math", "x = y + z", None);
503 assert_eq!(validator.validate(&role), RoleValidationResult::Valid);
504
505 let role = create_test_role("math", "", None);
507 assert!(matches!(
508 validator.validate(&role),
509 RoleValidationResult::Error(_)
510 ));
511
512 let role = create_test_role("math", "x = \\frac{a}{b", None);
514 assert!(matches!(
515 validator.validate(&role),
516 RoleValidationResult::Warning(_)
517 ));
518 }
519
520 #[test]
521 fn test_kbd_role_validator() {
522 let validator = KbdRoleValidator::new();
523
524 let role = create_test_role("kbd", "Ctrl+C", None);
526 assert_eq!(validator.validate(&role), RoleValidationResult::Valid);
527
528 let role = create_test_role("kbd", "", None);
530 assert!(matches!(
531 validator.validate(&role),
532 RoleValidationResult::Error(_)
533 ));
534 }
535}