1use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
2use crate::rule_config_serde::RuleConfig;
3use crate::rules::front_matter_utils::{FrontMatterType, FrontMatterUtils};
4use regex::Regex;
5use serde::{Deserialize, Serialize};
6use std::sync::LazyLock;
7
8static JSON_KEY_PATTERN: LazyLock<Regex> =
10 LazyLock::new(|| Regex::new(r#"^\s*"([^"]+)"\s*:"#).expect("Invalid JSON key regex"));
11
12#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
17pub struct MD072Config {
18 #[serde(default)]
20 pub enabled: bool,
21
22 #[serde(default, alias = "key-order")]
28 pub key_order: Option<Vec<String>>,
29}
30
31impl RuleConfig for MD072Config {
32 const RULE_NAME: &'static str = "MD072";
33}
34
35#[derive(Clone, Default)]
48pub struct MD072FrontmatterKeySort {
49 config: MD072Config,
50}
51
52impl MD072FrontmatterKeySort {
53 pub fn new() -> Self {
54 Self::default()
55 }
56
57 pub fn from_config_struct(config: MD072Config) -> Self {
59 Self { config }
60 }
61
62 fn has_comments(frontmatter_lines: &[&str]) -> bool {
64 frontmatter_lines.iter().any(|line| line.trim_start().starts_with('#'))
65 }
66
67 fn extract_yaml_keys(frontmatter_lines: &[&str]) -> Vec<(usize, String)> {
69 let mut keys = Vec::new();
70
71 for (idx, line) in frontmatter_lines.iter().enumerate() {
72 if !line.starts_with(' ')
74 && !line.starts_with('\t')
75 && let Some(colon_pos) = line.find(':')
76 {
77 let key = line[..colon_pos].trim();
78 if !key.is_empty() && !key.starts_with('#') {
79 keys.push((idx, key.to_string()));
80 }
81 }
82 }
83
84 keys
85 }
86
87 fn extract_toml_keys(frontmatter_lines: &[&str]) -> Vec<(usize, String)> {
89 let mut keys = Vec::new();
90
91 for (idx, line) in frontmatter_lines.iter().enumerate() {
92 let trimmed = line.trim();
93 if trimmed.is_empty() || trimmed.starts_with('#') {
95 continue;
96 }
97 if trimmed.starts_with('[') {
99 break;
100 }
101 if !line.starts_with(' ')
103 && !line.starts_with('\t')
104 && let Some(eq_pos) = line.find('=')
105 {
106 let key = line[..eq_pos].trim();
107 if !key.is_empty() {
108 keys.push((idx, key.to_string()));
109 }
110 }
111 }
112
113 keys
114 }
115
116 fn extract_json_keys(frontmatter_lines: &[&str]) -> Vec<String> {
118 let mut keys = Vec::new();
123 let mut depth: usize = 0;
124
125 for line in frontmatter_lines {
126 let line_start_depth = depth;
128
129 let mut in_string = false;
131 let mut prev_backslash = false;
132 for ch in line.chars() {
133 if in_string {
134 if ch == '"' && !prev_backslash {
135 in_string = false;
136 }
137 prev_backslash = ch == '\\' && !prev_backslash;
138 } else {
139 match ch {
140 '"' => in_string = true,
141 '{' | '[' => depth += 1,
142 '}' | ']' => depth = depth.saturating_sub(1),
143 _ => {}
144 }
145 prev_backslash = false;
146 }
147 }
148
149 if line_start_depth == 0
151 && let Some(captures) = JSON_KEY_PATTERN.captures(line)
152 && let Some(key_match) = captures.get(1)
153 {
154 keys.push(key_match.as_str().to_string());
155 }
156 }
157
158 keys
159 }
160
161 fn key_sort_position(key: &str, key_order: Option<&[String]>) -> (usize, String) {
165 if let Some(order) = key_order {
166 let key_lower = key.to_lowercase();
168 for (idx, ordered_key) in order.iter().enumerate() {
169 if ordered_key.to_lowercase() == key_lower {
170 return (idx, key_lower);
171 }
172 }
173 (usize::MAX, key_lower)
175 } else {
176 (0, key.to_lowercase())
178 }
179 }
180
181 fn find_first_unsorted_pair<'a>(keys: &'a [String], key_order: Option<&[String]>) -> Option<(&'a str, &'a str)> {
184 for i in 1..keys.len() {
185 let pos_curr = Self::key_sort_position(&keys[i], key_order);
186 let pos_prev = Self::key_sort_position(&keys[i - 1], key_order);
187 if pos_curr < pos_prev {
188 return Some((&keys[i], &keys[i - 1]));
189 }
190 }
191 None
192 }
193
194 fn find_first_unsorted_indexed_pair<'a>(
197 keys: &'a [(usize, String)],
198 key_order: Option<&[String]>,
199 ) -> Option<(usize, &'a str, &'a str)> {
200 for i in 1..keys.len() {
201 let pos_curr = Self::key_sort_position(&keys[i].1, key_order);
202 let pos_prev = Self::key_sort_position(&keys[i - 1].1, key_order);
203 if pos_curr < pos_prev {
204 return Some((keys[i].0, &keys[i].1, &keys[i - 1].1));
205 }
206 }
207 None
208 }
209
210 fn are_keys_sorted(keys: &[String], key_order: Option<&[String]>) -> bool {
212 Self::find_first_unsorted_pair(keys, key_order).is_none()
213 }
214
215 fn are_indexed_keys_sorted(keys: &[(usize, String)], key_order: Option<&[String]>) -> bool {
217 Self::find_first_unsorted_indexed_pair(keys, key_order).is_none()
218 }
219
220 fn sort_keys_by_order(keys: &mut [(String, Vec<&str>)], key_order: Option<&[String]>) {
222 keys.sort_by(|a, b| {
223 let pos_a = Self::key_sort_position(&a.0, key_order);
224 let pos_b = Self::key_sort_position(&b.0, key_order);
225 pos_a.cmp(&pos_b)
226 });
227 }
228}
229
230impl Rule for MD072FrontmatterKeySort {
231 fn name(&self) -> &'static str {
232 "MD072"
233 }
234
235 fn description(&self) -> &'static str {
236 "Frontmatter keys should be sorted alphabetically"
237 }
238
239 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
240 let content = ctx.content;
241 let mut warnings = Vec::new();
242
243 if content.is_empty() {
244 return Ok(warnings);
245 }
246
247 let fm_type = FrontMatterUtils::detect_front_matter_type(content);
248
249 match fm_type {
250 FrontMatterType::Yaml => {
251 let frontmatter_lines = FrontMatterUtils::extract_front_matter(content);
252 if frontmatter_lines.is_empty() {
253 return Ok(warnings);
254 }
255
256 let keys = Self::extract_yaml_keys(&frontmatter_lines);
257 let key_order = self.config.key_order.as_deref();
258 let Some((key_idx, out_of_place, should_come_after)) =
259 Self::find_first_unsorted_indexed_pair(&keys, key_order)
260 else {
261 return Ok(warnings);
262 };
263 let key_line = key_idx + 2;
265
266 let has_comments = Self::has_comments(&frontmatter_lines);
267
268 let fix = if has_comments {
269 None
270 } else {
271 let fixed_content = self.fix_yaml(content);
273 if fixed_content != content {
274 Some(Fix {
275 range: 0..content.len(),
276 replacement: fixed_content,
277 })
278 } else {
279 None
280 }
281 };
282
283 let message = if has_comments {
284 format!(
285 "YAML frontmatter keys are not sorted alphabetically: '{out_of_place}' should come before '{should_come_after}' (auto-fix unavailable: contains comments)"
286 )
287 } else {
288 format!(
289 "YAML frontmatter keys are not sorted alphabetically: '{out_of_place}' should come before '{should_come_after}'"
290 )
291 };
292
293 warnings.push(LintWarning {
294 rule_name: Some(self.name().to_string()),
295 message,
296 line: key_line,
297 column: 1,
298 end_line: key_line,
299 end_column: out_of_place.len() + 1,
300 severity: Severity::Warning,
301 fix,
302 });
303 }
304 FrontMatterType::Toml => {
305 let frontmatter_lines = FrontMatterUtils::extract_front_matter(content);
306 if frontmatter_lines.is_empty() {
307 return Ok(warnings);
308 }
309
310 let keys = Self::extract_toml_keys(&frontmatter_lines);
311 let key_order = self.config.key_order.as_deref();
312 let Some((key_idx, out_of_place, should_come_after)) =
313 Self::find_first_unsorted_indexed_pair(&keys, key_order)
314 else {
315 return Ok(warnings);
316 };
317 let key_line = key_idx + 2;
318
319 let has_comments = Self::has_comments(&frontmatter_lines);
320
321 let fix = if has_comments {
322 None
323 } else {
324 let fixed_content = self.fix_toml(content);
326 if fixed_content != content {
327 Some(Fix {
328 range: 0..content.len(),
329 replacement: fixed_content,
330 })
331 } else {
332 None
333 }
334 };
335
336 let message = if has_comments {
337 format!(
338 "TOML frontmatter keys are not sorted alphabetically: '{out_of_place}' should come before '{should_come_after}' (auto-fix unavailable: contains comments)"
339 )
340 } else {
341 format!(
342 "TOML frontmatter keys are not sorted alphabetically: '{out_of_place}' should come before '{should_come_after}'"
343 )
344 };
345
346 warnings.push(LintWarning {
347 rule_name: Some(self.name().to_string()),
348 message,
349 line: key_line,
350 column: 1,
351 end_line: key_line,
352 end_column: out_of_place.len() + 1,
353 severity: Severity::Warning,
354 fix,
355 });
356 }
357 FrontMatterType::Json => {
358 let frontmatter_lines = FrontMatterUtils::extract_front_matter(content);
359 if frontmatter_lines.is_empty() {
360 return Ok(warnings);
361 }
362
363 let keys = Self::extract_json_keys(&frontmatter_lines);
364 let key_order = self.config.key_order.as_deref();
365 let Some((out_of_place, should_come_after)) = Self::find_first_unsorted_pair(&keys, key_order) else {
366 return Ok(warnings);
367 };
368
369 let fixed_content = self.fix_json(content);
371 let fix = if fixed_content != content {
372 Some(Fix {
373 range: 0..content.len(),
374 replacement: fixed_content,
375 })
376 } else {
377 None
378 };
379
380 let message = format!(
381 "JSON frontmatter keys are not sorted alphabetically: '{out_of_place}' should come before '{should_come_after}'"
382 );
383
384 warnings.push(LintWarning {
385 rule_name: Some(self.name().to_string()),
386 message,
387 line: 2,
388 column: 1,
389 end_line: 2,
390 end_column: out_of_place.len() + 1,
391 severity: Severity::Warning,
392 fix,
393 });
394 }
395 _ => {
396 }
398 }
399
400 Ok(warnings)
401 }
402
403 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
404 let content = ctx.content;
405
406 if ctx.is_rule_disabled(self.name(), 2) {
408 return Ok(content.to_string());
409 }
410
411 let fm_type = FrontMatterUtils::detect_front_matter_type(content);
412
413 Ok(match fm_type {
414 FrontMatterType::Yaml => self.fix_yaml(content),
415 FrontMatterType::Toml => self.fix_toml(content),
416 FrontMatterType::Json => self.fix_json(content),
417 _ => content.to_string(),
418 })
419 }
420
421 fn category(&self) -> RuleCategory {
422 RuleCategory::FrontMatter
423 }
424
425 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
426 ctx.content.is_empty()
427 || !ctx.content.starts_with("---") && !ctx.content.starts_with("+++") && !ctx.content.starts_with('{')
428 }
429
430 fn as_any(&self) -> &dyn std::any::Any {
431 self
432 }
433
434 fn default_config_section(&self) -> Option<(String, toml::Value)> {
435 let table = crate::rule_config_serde::config_schema_table(&MD072Config::default())?;
436 Some((MD072Config::RULE_NAME.to_string(), toml::Value::Table(table)))
437 }
438
439 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
440 where
441 Self: Sized,
442 {
443 let rule_config = crate::rule_config_serde::load_rule_config::<MD072Config>(config);
444 Box::new(Self::from_config_struct(rule_config))
445 }
446}
447
448impl MD072FrontmatterKeySort {
449 fn fix_yaml(&self, content: &str) -> String {
450 let frontmatter_lines = FrontMatterUtils::extract_front_matter(content);
451 if frontmatter_lines.is_empty() {
452 return content.to_string();
453 }
454
455 if Self::has_comments(&frontmatter_lines) {
457 return content.to_string();
458 }
459
460 let keys = Self::extract_yaml_keys(&frontmatter_lines);
461 let key_order = self.config.key_order.as_deref();
462 if Self::are_indexed_keys_sorted(&keys, key_order) {
463 return content.to_string();
464 }
465
466 let mut key_blocks: Vec<(String, Vec<&str>)> = Vec::new();
469
470 for (i, (line_idx, key)) in keys.iter().enumerate() {
471 let start = *line_idx;
472 let end = if i + 1 < keys.len() {
473 keys[i + 1].0
474 } else {
475 frontmatter_lines.len()
476 };
477
478 let block_lines: Vec<&str> = frontmatter_lines[start..end].to_vec();
479 key_blocks.push((key.clone(), block_lines));
480 }
481
482 Self::sort_keys_by_order(&mut key_blocks, key_order);
484
485 let content_lines: Vec<&str> = content.lines().collect();
487 let fm_end = FrontMatterUtils::get_front_matter_end_line(content);
488
489 let mut result = String::new();
490 result.push_str("---\n");
491 for (_, lines) in &key_blocks {
492 for line in lines {
493 result.push_str(line);
494 result.push('\n');
495 }
496 }
497 result.push_str("---");
498
499 if fm_end < content_lines.len() {
500 result.push('\n');
501 result.push_str(&content_lines[fm_end..].join("\n"));
502 }
503
504 result
505 }
506
507 fn fix_toml(&self, content: &str) -> String {
508 let frontmatter_lines = FrontMatterUtils::extract_front_matter(content);
509 if frontmatter_lines.is_empty() {
510 return content.to_string();
511 }
512
513 if Self::has_comments(&frontmatter_lines) {
515 return content.to_string();
516 }
517
518 let keys = Self::extract_toml_keys(&frontmatter_lines);
519 let key_order = self.config.key_order.as_deref();
520 if Self::are_indexed_keys_sorted(&keys, key_order) {
521 return content.to_string();
522 }
523
524 let mut key_blocks: Vec<(String, Vec<&str>)> = Vec::new();
527
528 for (i, (line_idx, key)) in keys.iter().enumerate() {
529 let start = *line_idx;
530 let end = if i + 1 < keys.len() {
531 keys[i + 1].0
532 } else {
533 frontmatter_lines.len()
534 };
535
536 let block_lines: Vec<&str> = frontmatter_lines[start..end].to_vec();
537 key_blocks.push((key.clone(), block_lines));
538 }
539
540 Self::sort_keys_by_order(&mut key_blocks, key_order);
542
543 let content_lines: Vec<&str> = content.lines().collect();
545 let fm_end = FrontMatterUtils::get_front_matter_end_line(content);
546
547 let mut result = String::new();
548 result.push_str("+++\n");
549 for (_, lines) in &key_blocks {
550 for line in lines {
551 result.push_str(line);
552 result.push('\n');
553 }
554 }
555 result.push_str("+++");
556
557 if fm_end < content_lines.len() {
558 result.push('\n');
559 result.push_str(&content_lines[fm_end..].join("\n"));
560 }
561
562 result
563 }
564
565 fn fix_json(&self, content: &str) -> String {
566 let frontmatter_lines = FrontMatterUtils::extract_front_matter(content);
567 if frontmatter_lines.is_empty() {
568 return content.to_string();
569 }
570
571 let keys = Self::extract_json_keys(&frontmatter_lines);
572 let key_order = self.config.key_order.as_deref();
573
574 if keys.is_empty() || Self::are_keys_sorted(&keys, key_order) {
575 return content.to_string();
576 }
577
578 let json_content = format!("{{{}}}", frontmatter_lines.join("\n"));
580
581 match serde_json::from_str::<serde_json::Value>(&json_content) {
583 Ok(serde_json::Value::Object(map)) => {
584 let mut sorted_map = serde_json::Map::new();
586 let mut keys: Vec<_> = map.keys().cloned().collect();
587 keys.sort_by(|a, b| {
588 let pos_a = Self::key_sort_position(a, key_order);
589 let pos_b = Self::key_sort_position(b, key_order);
590 pos_a.cmp(&pos_b)
591 });
592
593 for key in keys {
594 if let Some(value) = map.get(&key) {
595 sorted_map.insert(key, value.clone());
596 }
597 }
598
599 match serde_json::to_string_pretty(&serde_json::Value::Object(sorted_map)) {
600 Ok(sorted_json) => {
601 let lines: Vec<&str> = content.lines().collect();
602 let fm_end = FrontMatterUtils::get_front_matter_end_line(content);
603
604 let mut result = String::new();
607 result.push_str(&sorted_json);
608
609 if fm_end < lines.len() {
610 result.push('\n');
611 result.push_str(&lines[fm_end..].join("\n"));
612 }
613
614 result
615 }
616 Err(_) => content.to_string(),
617 }
618 }
619 _ => content.to_string(),
620 }
621 }
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627 use crate::lint_context::LintContext;
628
629 fn create_enabled_rule() -> MD072FrontmatterKeySort {
631 MD072FrontmatterKeySort::from_config_struct(MD072Config {
632 enabled: true,
633 key_order: None,
634 })
635 }
636
637 fn create_rule_with_key_order(keys: Vec<&str>) -> MD072FrontmatterKeySort {
639 MD072FrontmatterKeySort::from_config_struct(MD072Config {
640 enabled: true,
641 key_order: Some(keys.into_iter().map(String::from).collect()),
642 })
643 }
644
645 #[test]
648 fn test_enabled_via_config() {
649 let rule = create_enabled_rule();
650 let content = "---\ntitle: Test\nauthor: John\n---\n\n# Heading";
651 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
652 let result = rule.check(&ctx).unwrap();
653
654 assert_eq!(result.len(), 1);
656 }
657
658 #[test]
661 fn test_no_frontmatter() {
662 let rule = create_enabled_rule();
663 let content = "# Heading\n\nContent.";
664 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
665 let result = rule.check(&ctx).unwrap();
666
667 assert!(result.is_empty());
668 }
669
670 #[test]
671 fn test_yaml_sorted_keys() {
672 let rule = create_enabled_rule();
673 let content = "---\nauthor: John\ndate: 2024-01-01\ntitle: Test\n---\n\n# Heading";
674 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
675 let result = rule.check(&ctx).unwrap();
676
677 assert!(result.is_empty());
678 }
679
680 #[test]
681 fn test_yaml_unsorted_keys() {
682 let rule = create_enabled_rule();
683 let content = "---\ntitle: Test\nauthor: John\ndate: 2024-01-01\n---\n\n# Heading";
684 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
685 let result = rule.check(&ctx).unwrap();
686
687 assert_eq!(result.len(), 1);
688 assert!(result[0].message.contains("YAML"));
689 assert!(result[0].message.contains("not sorted"));
690 assert!(result[0].message.contains("'author' should come before 'title'"));
692 }
693
694 #[test]
695 fn test_yaml_case_insensitive_sort() {
696 let rule = create_enabled_rule();
697 let content = "---\nAuthor: John\ndate: 2024-01-01\nTitle: Test\n---\n\n# Heading";
698 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
699 let result = rule.check(&ctx).unwrap();
700
701 assert!(result.is_empty());
703 }
704
705 #[test]
706 fn test_yaml_fix_sorts_keys() {
707 let rule = create_enabled_rule();
708 let content = "---\ntitle: Test\nauthor: John\n---\n\n# Heading";
709 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
710 let fixed = rule.fix(&ctx).unwrap();
711
712 let author_pos = fixed.find("author:").unwrap();
714 let title_pos = fixed.find("title:").unwrap();
715 assert!(author_pos < title_pos);
716 }
717
718 #[test]
719 fn test_yaml_no_fix_with_comments() {
720 let rule = create_enabled_rule();
721 let content = "---\ntitle: Test\n# This is a comment\nauthor: John\n---\n\n# Heading";
722 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
723 let result = rule.check(&ctx).unwrap();
724
725 assert_eq!(result.len(), 1);
726 assert!(result[0].message.contains("auto-fix unavailable"));
727 assert!(result[0].fix.is_none());
728
729 let fixed = rule.fix(&ctx).unwrap();
731 assert_eq!(fixed, content);
732 }
733
734 #[test]
735 fn test_yaml_single_key() {
736 let rule = create_enabled_rule();
737 let content = "---\ntitle: Test\n---\n\n# Heading";
738 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
739 let result = rule.check(&ctx).unwrap();
740
741 assert!(result.is_empty());
743 }
744
745 #[test]
746 fn test_yaml_nested_keys_ignored() {
747 let rule = create_enabled_rule();
748 let content = "---\nauthor:\n name: John\n email: john@example.com\ntitle: Test\n---\n\n# Heading";
750 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
751 let result = rule.check(&ctx).unwrap();
752
753 assert!(result.is_empty());
755 }
756
757 #[test]
758 fn test_yaml_fix_idempotent() {
759 let rule = create_enabled_rule();
760 let content = "---\ntitle: Test\nauthor: John\n---\n\n# Heading";
761 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
762 let fixed_once = rule.fix(&ctx).unwrap();
763
764 let ctx2 = LintContext::new(&fixed_once, crate::config::MarkdownFlavor::Standard, None);
765 let fixed_twice = rule.fix(&ctx2).unwrap();
766
767 assert_eq!(fixed_once, fixed_twice);
768 }
769
770 #[test]
771 fn test_yaml_complex_values() {
772 let rule = create_enabled_rule();
773 let content =
775 "---\nauthor: John Doe\ntags:\n - rust\n - markdown\ntitle: \"Test: A Complex Title\"\n---\n\n# Heading";
776 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
777 let result = rule.check(&ctx).unwrap();
778
779 assert!(result.is_empty());
781 }
782
783 #[test]
786 fn test_toml_sorted_keys() {
787 let rule = create_enabled_rule();
788 let content = "+++\nauthor = \"John\"\ndate = \"2024-01-01\"\ntitle = \"Test\"\n+++\n\n# Heading";
789 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
790 let result = rule.check(&ctx).unwrap();
791
792 assert!(result.is_empty());
793 }
794
795 #[test]
796 fn test_toml_unsorted_keys() {
797 let rule = create_enabled_rule();
798 let content = "+++\ntitle = \"Test\"\nauthor = \"John\"\n+++\n\n# Heading";
799 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
800 let result = rule.check(&ctx).unwrap();
801
802 assert_eq!(result.len(), 1);
803 assert!(result[0].message.contains("TOML"));
804 assert!(result[0].message.contains("not sorted"));
805 }
806
807 #[test]
808 fn test_toml_fix_sorts_keys() {
809 let rule = create_enabled_rule();
810 let content = "+++\ntitle = \"Test\"\nauthor = \"John\"\n+++\n\n# Heading";
811 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
812 let fixed = rule.fix(&ctx).unwrap();
813
814 let author_pos = fixed.find("author").unwrap();
816 let title_pos = fixed.find("title").unwrap();
817 assert!(author_pos < title_pos);
818 }
819
820 #[test]
821 fn test_toml_no_fix_with_comments() {
822 let rule = create_enabled_rule();
823 let content = "+++\ntitle = \"Test\"\n# This is a comment\nauthor = \"John\"\n+++\n\n# Heading";
824 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
825 let result = rule.check(&ctx).unwrap();
826
827 assert_eq!(result.len(), 1);
828 assert!(result[0].message.contains("auto-fix unavailable"));
829
830 let fixed = rule.fix(&ctx).unwrap();
832 assert_eq!(fixed, content);
833 }
834
835 #[test]
838 fn test_json_sorted_keys() {
839 let rule = create_enabled_rule();
840 let content = "{\n\"author\": \"John\",\n\"title\": \"Test\"\n}\n\n# Heading";
841 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
842 let result = rule.check(&ctx).unwrap();
843
844 assert!(result.is_empty());
845 }
846
847 #[test]
848 fn test_json_unsorted_keys() {
849 let rule = create_enabled_rule();
850 let content = "{\n\"title\": \"Test\",\n\"author\": \"John\"\n}\n\n# Heading";
851 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
852 let result = rule.check(&ctx).unwrap();
853
854 assert_eq!(result.len(), 1);
855 assert!(result[0].message.contains("JSON"));
856 assert!(result[0].message.contains("not sorted"));
857 }
858
859 #[test]
860 fn test_json_fix_sorts_keys() {
861 let rule = create_enabled_rule();
862 let content = "{\n\"title\": \"Test\",\n\"author\": \"John\"\n}\n\n# Heading";
863 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
864 let fixed = rule.fix(&ctx).unwrap();
865
866 let author_pos = fixed.find("author").unwrap();
868 let title_pos = fixed.find("title").unwrap();
869 assert!(author_pos < title_pos);
870 }
871
872 #[test]
873 fn test_json_always_fixable() {
874 let rule = create_enabled_rule();
875 let content = "{\n\"title\": \"Test\",\n\"author\": \"John\"\n}\n\n# Heading";
877 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
878 let result = rule.check(&ctx).unwrap();
879
880 assert_eq!(result.len(), 1);
881 assert!(result[0].fix.is_some()); assert!(!result[0].message.contains("Auto-fix unavailable"));
883 }
884
885 #[test]
888 fn test_empty_content() {
889 let rule = create_enabled_rule();
890 let content = "";
891 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
892 let result = rule.check(&ctx).unwrap();
893
894 assert!(result.is_empty());
895 }
896
897 #[test]
898 fn test_empty_frontmatter() {
899 let rule = create_enabled_rule();
900 let content = "---\n---\n\n# Heading";
901 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
902 let result = rule.check(&ctx).unwrap();
903
904 assert!(result.is_empty());
905 }
906
907 #[test]
908 fn test_toml_nested_tables_ignored() {
909 let rule = create_enabled_rule();
911 let content = "+++\ntitle = \"Programming\"\nsort_by = \"weight\"\n\n[extra]\nwe_have_extra = \"variables\"\n+++\n\n# Heading";
912 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
913 let result = rule.check(&ctx).unwrap();
914
915 assert_eq!(result.len(), 1);
917 assert!(result[0].message.contains("'sort_by' should come before 'title'"));
919 assert!(!result[0].message.contains("we_have_extra"));
920 }
921
922 #[test]
923 fn test_toml_nested_taxonomies_ignored() {
924 let rule = create_enabled_rule();
926 let content = "+++\ntitle = \"Test\"\ndate = \"2024-01-01\"\n\n[taxonomies]\ncategories = [\"test\"]\ntags = [\"foo\"]\n+++\n\n# Heading";
927 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
928 let result = rule.check(&ctx).unwrap();
929
930 assert_eq!(result.len(), 1);
932 assert!(result[0].message.contains("'date' should come before 'title'"));
934 assert!(!result[0].message.contains("categories"));
935 assert!(!result[0].message.contains("tags"));
936 }
937
938 #[test]
941 fn test_yaml_unicode_keys() {
942 let rule = create_enabled_rule();
943 let content = "---\nタイトル: Test\nあいう: Value\n日本語: Content\n---\n\n# Heading";
945 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
946 let result = rule.check(&ctx).unwrap();
947
948 assert_eq!(result.len(), 1);
950 }
951
952 #[test]
953 fn test_yaml_keys_with_special_characters() {
954 let rule = create_enabled_rule();
955 let content = "---\nmy-key: value1\nmy_key: value2\nmykey: value3\n---\n\n# Heading";
957 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
958 let result = rule.check(&ctx).unwrap();
959
960 assert!(result.is_empty());
962 }
963
964 #[test]
965 fn test_yaml_keys_with_numbers() {
966 let rule = create_enabled_rule();
967 let content = "---\nkey1: value\nkey10: value\nkey2: value\n---\n\n# Heading";
968 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
969 let result = rule.check(&ctx).unwrap();
970
971 assert!(result.is_empty());
973 }
974
975 #[test]
976 fn test_yaml_multiline_string_block_literal() {
977 let rule = create_enabled_rule();
978 let content =
979 "---\ndescription: |\n This is a\n multiline literal\ntitle: Test\nauthor: John\n---\n\n# Heading";
980 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
981 let result = rule.check(&ctx).unwrap();
982
983 assert_eq!(result.len(), 1);
985 assert!(result[0].message.contains("'author' should come before 'title'"));
986 }
987
988 #[test]
989 fn test_yaml_multiline_string_folded() {
990 let rule = create_enabled_rule();
991 let content = "---\ndescription: >\n This is a\n folded string\nauthor: John\n---\n\n# Heading";
992 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
993 let result = rule.check(&ctx).unwrap();
994
995 assert_eq!(result.len(), 1);
997 }
998
999 #[test]
1000 fn test_yaml_fix_preserves_multiline_values() {
1001 let rule = create_enabled_rule();
1002 let content = "---\ntitle: Test\ndescription: |\n Line 1\n Line 2\n---\n\n# Heading";
1003 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1004 let fixed = rule.fix(&ctx).unwrap();
1005
1006 let desc_pos = fixed.find("description").unwrap();
1008 let title_pos = fixed.find("title").unwrap();
1009 assert!(desc_pos < title_pos);
1010 }
1011
1012 #[test]
1013 fn test_yaml_quoted_keys() {
1014 let rule = create_enabled_rule();
1015 let content = "---\n\"quoted-key\": value1\nunquoted: value2\n---\n\n# Heading";
1016 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1017 let result = rule.check(&ctx).unwrap();
1018
1019 assert!(result.is_empty());
1021 }
1022
1023 #[test]
1024 fn test_yaml_duplicate_keys() {
1025 let rule = create_enabled_rule();
1027 let content = "---\ntitle: First\nauthor: John\ntitle: Second\n---\n\n# Heading";
1028 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1029 let result = rule.check(&ctx).unwrap();
1030
1031 assert_eq!(result.len(), 1);
1033 }
1034
1035 #[test]
1036 fn test_toml_inline_table() {
1037 let rule = create_enabled_rule();
1038 let content =
1039 "+++\nauthor = { name = \"John\", email = \"john@example.com\" }\ntitle = \"Test\"\n+++\n\n# Heading";
1040 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1041 let result = rule.check(&ctx).unwrap();
1042
1043 assert!(result.is_empty());
1045 }
1046
1047 #[test]
1048 fn test_toml_array_of_tables() {
1049 let rule = create_enabled_rule();
1050 let content = "+++\ntitle = \"Test\"\ndate = \"2024-01-01\"\n\n[[authors]]\nname = \"John\"\n\n[[authors]]\nname = \"Jane\"\n+++\n\n# Heading";
1051 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1052 let result = rule.check(&ctx).unwrap();
1053
1054 assert_eq!(result.len(), 1);
1056 assert!(result[0].message.contains("'date' should come before 'title'"));
1058 }
1059
1060 #[test]
1061 fn test_json_nested_objects() {
1062 let rule = create_enabled_rule();
1063 let content = "{\n\"author\": {\n \"name\": \"John\",\n \"email\": \"john@example.com\"\n},\n\"title\": \"Test\"\n}\n\n# Heading";
1064 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1065 let result = rule.check(&ctx).unwrap();
1066
1067 assert!(result.is_empty());
1069 }
1070
1071 #[test]
1072 fn test_json_arrays() {
1073 let rule = create_enabled_rule();
1074 let content = "{\n\"tags\": [\"rust\", \"markdown\"],\n\"author\": \"John\"\n}\n\n# Heading";
1075 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1076 let result = rule.check(&ctx).unwrap();
1077
1078 assert_eq!(result.len(), 1);
1080 }
1081
1082 #[test]
1083 fn test_fix_preserves_content_after_frontmatter() {
1084 let rule = create_enabled_rule();
1085 let content = "---\ntitle: Test\nauthor: John\n---\n\n# Heading\n\nParagraph 1.\n\n- List item\n- Another item";
1086 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1087 let fixed = rule.fix(&ctx).unwrap();
1088
1089 assert!(fixed.contains("# Heading"));
1091 assert!(fixed.contains("Paragraph 1."));
1092 assert!(fixed.contains("- List item"));
1093 assert!(fixed.contains("- Another item"));
1094 }
1095
1096 #[test]
1097 fn test_fix_yaml_produces_valid_yaml() {
1098 let rule = create_enabled_rule();
1099 let content = "---\ntitle: \"Test: A Title\"\nauthor: John Doe\ndate: 2024-01-15\n---\n\n# Heading";
1100 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1101 let fixed = rule.fix(&ctx).unwrap();
1102
1103 let lines: Vec<&str> = fixed.lines().collect();
1106 let fm_end = lines.iter().skip(1).position(|l| *l == "---").unwrap() + 1;
1107 let fm_content: String = lines[1..fm_end].join("\n");
1108
1109 let parsed: Result<serde_yml::Value, _> = serde_yml::from_str(&fm_content);
1111 assert!(parsed.is_ok(), "Fixed YAML should be valid: {fm_content}");
1112 }
1113
1114 #[test]
1115 fn test_fix_toml_produces_valid_toml() {
1116 let rule = create_enabled_rule();
1117 let content = "+++\ntitle = \"Test\"\nauthor = \"John Doe\"\ndate = 2024-01-15\n+++\n\n# Heading";
1118 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1119 let fixed = rule.fix(&ctx).unwrap();
1120
1121 let lines: Vec<&str> = fixed.lines().collect();
1123 let fm_end = lines.iter().skip(1).position(|l| *l == "+++").unwrap() + 1;
1124 let fm_content: String = lines[1..fm_end].join("\n");
1125
1126 let parsed: Result<toml::Value, _> = toml::from_str(&fm_content);
1128 assert!(parsed.is_ok(), "Fixed TOML should be valid: {fm_content}");
1129 }
1130
1131 #[test]
1132 fn test_fix_json_produces_valid_json() {
1133 let rule = create_enabled_rule();
1134 let content = "{\n\"title\": \"Test\",\n\"author\": \"John\"\n}\n\n# Heading";
1135 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1136 let fixed = rule.fix(&ctx).unwrap();
1137
1138 let json_end = fixed.find("\n\n").unwrap();
1140 let json_content = &fixed[..json_end];
1141
1142 let parsed: Result<serde_json::Value, _> = serde_json::from_str(json_content);
1144 assert!(parsed.is_ok(), "Fixed JSON should be valid: {json_content}");
1145 }
1146
1147 #[test]
1148 fn test_many_keys_performance() {
1149 let rule = create_enabled_rule();
1150 let mut keys: Vec<String> = (0..100).map(|i| format!("key{i:03}: value{i}")).collect();
1152 keys.reverse(); let content = format!("---\n{}\n---\n\n# Heading", keys.join("\n"));
1154
1155 let ctx = LintContext::new(&content, crate::config::MarkdownFlavor::Standard, None);
1156 let result = rule.check(&ctx).unwrap();
1157
1158 assert_eq!(result.len(), 1);
1160 }
1161
1162 #[test]
1163 fn test_yaml_empty_value() {
1164 let rule = create_enabled_rule();
1165 let content = "---\ntitle:\nauthor: John\n---\n\n# Heading";
1166 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1167 let result = rule.check(&ctx).unwrap();
1168
1169 assert_eq!(result.len(), 1);
1171 }
1172
1173 #[test]
1174 fn test_yaml_null_value() {
1175 let rule = create_enabled_rule();
1176 let content = "---\ntitle: null\nauthor: John\n---\n\n# Heading";
1177 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1178 let result = rule.check(&ctx).unwrap();
1179
1180 assert_eq!(result.len(), 1);
1181 }
1182
1183 #[test]
1184 fn test_yaml_boolean_values() {
1185 let rule = create_enabled_rule();
1186 let content = "---\ndraft: true\nauthor: John\n---\n\n# Heading";
1187 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1188 let result = rule.check(&ctx).unwrap();
1189
1190 assert_eq!(result.len(), 1);
1192 }
1193
1194 #[test]
1195 fn test_toml_boolean_values() {
1196 let rule = create_enabled_rule();
1197 let content = "+++\ndraft = true\nauthor = \"John\"\n+++\n\n# Heading";
1198 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1199 let result = rule.check(&ctx).unwrap();
1200
1201 assert_eq!(result.len(), 1);
1202 }
1203
1204 #[test]
1205 fn test_yaml_list_at_top_level() {
1206 let rule = create_enabled_rule();
1207 let content = "---\ntags:\n - rust\n - markdown\nauthor: John\n---\n\n# Heading";
1208 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1209 let result = rule.check(&ctx).unwrap();
1210
1211 assert_eq!(result.len(), 1);
1213 }
1214
1215 #[test]
1216 fn test_three_keys_all_orderings() {
1217 let rule = create_enabled_rule();
1218
1219 let orderings = [
1221 ("a, b, c", "---\na: 1\nb: 2\nc: 3\n---\n\n# H", true), ("a, c, b", "---\na: 1\nc: 3\nb: 2\n---\n\n# H", false), ("b, a, c", "---\nb: 2\na: 1\nc: 3\n---\n\n# H", false), ("b, c, a", "---\nb: 2\nc: 3\na: 1\n---\n\n# H", false), ("c, a, b", "---\nc: 3\na: 1\nb: 2\n---\n\n# H", false), ("c, b, a", "---\nc: 3\nb: 2\na: 1\n---\n\n# H", false), ];
1228
1229 for (name, content, should_pass) in orderings {
1230 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1231 let result = rule.check(&ctx).unwrap();
1232 assert_eq!(
1233 result.is_empty(),
1234 should_pass,
1235 "Ordering {name} should {} pass",
1236 if should_pass { "" } else { "not" }
1237 );
1238 }
1239 }
1240
1241 #[test]
1242 fn test_crlf_line_endings() {
1243 let rule = create_enabled_rule();
1244 let content = "---\r\ntitle: Test\r\nauthor: John\r\n---\r\n\r\n# Heading";
1245 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1246 let result = rule.check(&ctx).unwrap();
1247
1248 assert_eq!(result.len(), 1);
1250 }
1251
1252 #[test]
1253 fn test_json_escaped_quotes_in_keys() {
1254 let rule = create_enabled_rule();
1255 let content = "{\n\"normal\": \"value\",\n\"key\": \"with \\\"quotes\\\"\"\n}\n\n# Heading";
1257 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1258 let result = rule.check(&ctx).unwrap();
1259
1260 assert_eq!(result.len(), 1);
1262 }
1263
1264 #[test]
1267 fn test_warning_fix_yaml_sorts_keys() {
1268 let rule = create_enabled_rule();
1269 let content = "---\nbbb: 123\naaa:\n - hello\n - world\n---\n\n# Heading\n";
1270 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1271 let warnings = rule.check(&ctx).unwrap();
1272
1273 assert_eq!(warnings.len(), 1);
1274 assert!(warnings[0].fix.is_some(), "Warning should have a fix attached for LSP");
1275
1276 let fix = warnings[0].fix.as_ref().unwrap();
1277 assert_eq!(fix.range, 0..content.len(), "Fix should replace entire content");
1278
1279 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).expect("Fix should apply");
1281
1282 let aaa_pos = fixed.find("aaa:").expect("aaa should exist");
1284 let bbb_pos = fixed.find("bbb:").expect("bbb should exist");
1285 assert!(aaa_pos < bbb_pos, "aaa should come before bbb after sorting");
1286 }
1287
1288 #[test]
1289 fn test_warning_fix_preserves_yaml_list_indentation() {
1290 let rule = create_enabled_rule();
1291 let content = "---\nbbb: 123\naaa:\n - hello\n - world\n---\n\n# Heading\n";
1292 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1293 let warnings = rule.check(&ctx).unwrap();
1294
1295 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).expect("Fix should apply");
1296
1297 assert!(
1299 fixed.contains(" - hello"),
1300 "List indentation should be preserved: {fixed}"
1301 );
1302 assert!(
1303 fixed.contains(" - world"),
1304 "List indentation should be preserved: {fixed}"
1305 );
1306 }
1307
1308 #[test]
1309 fn test_warning_fix_preserves_nested_object_indentation() {
1310 let rule = create_enabled_rule();
1311 let content = "---\nzzzz: value\naaaa:\n nested_key: nested_value\n another: 123\n---\n\n# Heading\n";
1312 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1313 let warnings = rule.check(&ctx).unwrap();
1314
1315 assert_eq!(warnings.len(), 1);
1316 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).expect("Fix should apply");
1317
1318 let aaaa_pos = fixed.find("aaaa:").expect("aaaa should exist");
1320 let zzzz_pos = fixed.find("zzzz:").expect("zzzz should exist");
1321 assert!(aaaa_pos < zzzz_pos, "aaaa should come before zzzz");
1322
1323 assert!(
1325 fixed.contains(" nested_key: nested_value"),
1326 "Nested object indentation should be preserved: {fixed}"
1327 );
1328 assert!(
1329 fixed.contains(" another: 123"),
1330 "Nested object indentation should be preserved: {fixed}"
1331 );
1332 }
1333
1334 #[test]
1335 fn test_warning_fix_preserves_deeply_nested_structure() {
1336 let rule = create_enabled_rule();
1337 let content = "---\nzzz: top\naaa:\n level1:\n level2:\n - item1\n - item2\n---\n\n# Content\n";
1338 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1339 let warnings = rule.check(&ctx).unwrap();
1340
1341 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).expect("Fix should apply");
1342
1343 let aaa_pos = fixed.find("aaa:").expect("aaa should exist");
1345 let zzz_pos = fixed.find("zzz:").expect("zzz should exist");
1346 assert!(aaa_pos < zzz_pos, "aaa should come before zzz");
1347
1348 assert!(fixed.contains(" level1:"), "2-space indent should be preserved");
1350 assert!(fixed.contains(" level2:"), "4-space indent should be preserved");
1351 assert!(fixed.contains(" - item1"), "6-space indent should be preserved");
1352 assert!(fixed.contains(" - item2"), "6-space indent should be preserved");
1353 }
1354
1355 #[test]
1356 fn test_warning_fix_toml_sorts_keys() {
1357 let rule = create_enabled_rule();
1358 let content = "+++\ntitle = \"Test\"\nauthor = \"John\"\n+++\n\n# Heading\n";
1359 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1360 let warnings = rule.check(&ctx).unwrap();
1361
1362 assert_eq!(warnings.len(), 1);
1363 assert!(warnings[0].fix.is_some(), "TOML warning should have a fix");
1364
1365 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).expect("Fix should apply");
1366
1367 let author_pos = fixed.find("author").expect("author should exist");
1369 let title_pos = fixed.find("title").expect("title should exist");
1370 assert!(author_pos < title_pos, "author should come before title");
1371 }
1372
1373 #[test]
1374 fn test_warning_fix_json_sorts_keys() {
1375 let rule = create_enabled_rule();
1376 let content = "{\n\"title\": \"Test\",\n\"author\": \"John\"\n}\n\n# Heading\n";
1377 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1378 let warnings = rule.check(&ctx).unwrap();
1379
1380 assert_eq!(warnings.len(), 1);
1381 assert!(warnings[0].fix.is_some(), "JSON warning should have a fix");
1382
1383 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).expect("Fix should apply");
1384
1385 let author_pos = fixed.find("author").expect("author should exist");
1387 let title_pos = fixed.find("title").expect("title should exist");
1388 assert!(author_pos < title_pos, "author should come before title");
1389 }
1390
1391 #[test]
1392 fn test_warning_fix_no_fix_when_comments_present() {
1393 let rule = create_enabled_rule();
1394 let content = "---\ntitle: Test\n# This is a comment\nauthor: John\n---\n\n# Heading\n";
1395 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1396 let warnings = rule.check(&ctx).unwrap();
1397
1398 assert_eq!(warnings.len(), 1);
1399 assert!(
1400 warnings[0].fix.is_none(),
1401 "Warning should NOT have a fix when comments are present"
1402 );
1403 assert!(
1404 warnings[0].message.contains("auto-fix unavailable"),
1405 "Message should indicate auto-fix is unavailable"
1406 );
1407 }
1408
1409 #[test]
1410 fn test_warning_fix_preserves_content_after_frontmatter() {
1411 let rule = create_enabled_rule();
1412 let content = "---\nzzz: last\naaa: first\n---\n\n# Heading\n\nParagraph with content.\n\n- List item\n";
1413 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1414 let warnings = rule.check(&ctx).unwrap();
1415
1416 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).expect("Fix should apply");
1417
1418 assert!(fixed.contains("# Heading"), "Heading should be preserved");
1420 assert!(
1421 fixed.contains("Paragraph with content."),
1422 "Paragraph should be preserved"
1423 );
1424 assert!(fixed.contains("- List item"), "List item should be preserved");
1425 }
1426
1427 #[test]
1428 fn test_warning_fix_idempotent() {
1429 let rule = create_enabled_rule();
1430 let content = "---\nbbb: 2\naaa: 1\n---\n\n# Heading\n";
1431 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1432 let warnings = rule.check(&ctx).unwrap();
1433
1434 let fixed_once = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).expect("Fix should apply");
1435
1436 let ctx2 = LintContext::new(&fixed_once, crate::config::MarkdownFlavor::Standard, None);
1438 let warnings2 = rule.check(&ctx2).unwrap();
1439
1440 assert!(
1441 warnings2.is_empty(),
1442 "After fixing, no more warnings should be produced"
1443 );
1444 }
1445
1446 #[test]
1447 fn test_warning_fix_preserves_multiline_block_literal() {
1448 let rule = create_enabled_rule();
1449 let content = "---\nzzz: simple\naaa: |\n Line 1 of block\n Line 2 of block\n---\n\n# Heading\n";
1450 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1451 let warnings = rule.check(&ctx).unwrap();
1452
1453 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).expect("Fix should apply");
1454
1455 assert!(fixed.contains("aaa: |"), "Block literal marker should be preserved");
1457 assert!(
1458 fixed.contains(" Line 1 of block"),
1459 "Block literal line 1 should be preserved with indent"
1460 );
1461 assert!(
1462 fixed.contains(" Line 2 of block"),
1463 "Block literal line 2 should be preserved with indent"
1464 );
1465 }
1466
1467 #[test]
1468 fn test_warning_fix_preserves_folded_string() {
1469 let rule = create_enabled_rule();
1470 let content = "---\nzzz: simple\naaa: >\n Folded line 1\n Folded line 2\n---\n\n# Content\n";
1471 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1472 let warnings = rule.check(&ctx).unwrap();
1473
1474 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).expect("Fix should apply");
1475
1476 assert!(fixed.contains("aaa: >"), "Folded string marker should be preserved");
1478 assert!(
1479 fixed.contains(" Folded line 1"),
1480 "Folded line 1 should be preserved with indent"
1481 );
1482 assert!(
1483 fixed.contains(" Folded line 2"),
1484 "Folded line 2 should be preserved with indent"
1485 );
1486 }
1487
1488 #[test]
1489 fn test_warning_fix_preserves_4_space_indentation() {
1490 let rule = create_enabled_rule();
1491 let content = "---\nzzz: value\naaa:\n nested: with_4_spaces\n another: value\n---\n\n# Heading\n";
1493 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1494 let warnings = rule.check(&ctx).unwrap();
1495
1496 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).expect("Fix should apply");
1497
1498 assert!(
1500 fixed.contains(" nested: with_4_spaces"),
1501 "4-space indentation should be preserved: {fixed}"
1502 );
1503 assert!(
1504 fixed.contains(" another: value"),
1505 "4-space indentation should be preserved: {fixed}"
1506 );
1507 }
1508
1509 #[test]
1510 fn test_warning_fix_preserves_tab_indentation() {
1511 let rule = create_enabled_rule();
1512 let content = "---\nzzz: value\naaa:\n\tnested: with_tab\n\tanother: value\n---\n\n# Heading\n";
1514 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1515 let warnings = rule.check(&ctx).unwrap();
1516
1517 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).expect("Fix should apply");
1518
1519 assert!(
1521 fixed.contains("\tnested: with_tab"),
1522 "Tab indentation should be preserved: {fixed}"
1523 );
1524 assert!(
1525 fixed.contains("\tanother: value"),
1526 "Tab indentation should be preserved: {fixed}"
1527 );
1528 }
1529
1530 #[test]
1531 fn test_warning_fix_preserves_inline_list() {
1532 let rule = create_enabled_rule();
1533 let content = "---\nzzz: value\naaa: [one, two, three]\n---\n\n# Heading\n";
1535 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1536 let warnings = rule.check(&ctx).unwrap();
1537
1538 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).expect("Fix should apply");
1539
1540 assert!(
1542 fixed.contains("aaa: [one, two, three]"),
1543 "Inline list should be preserved exactly: {fixed}"
1544 );
1545 }
1546
1547 #[test]
1548 fn test_warning_fix_preserves_quoted_strings() {
1549 let rule = create_enabled_rule();
1550 let content = "---\nzzz: simple\naaa: \"value with: colon\"\nbbb: 'single quotes'\n---\n\n# Heading\n";
1552 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1553 let warnings = rule.check(&ctx).unwrap();
1554
1555 let fixed = crate::utils::fix_utils::apply_warning_fixes(content, &warnings).expect("Fix should apply");
1556
1557 assert!(
1559 fixed.contains("aaa: \"value with: colon\""),
1560 "Double-quoted string should be preserved: {fixed}"
1561 );
1562 assert!(
1563 fixed.contains("bbb: 'single quotes'"),
1564 "Single-quoted string should be preserved: {fixed}"
1565 );
1566 }
1567
1568 #[test]
1571 fn test_yaml_custom_key_order_sorted() {
1572 let rule = create_rule_with_key_order(vec!["title", "date", "author"]);
1574 let content = "---\ntitle: Test\ndate: 2024-01-01\nauthor: John\n---\n\n# Heading";
1575 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1576 let result = rule.check(&ctx).unwrap();
1577
1578 assert!(result.is_empty());
1580 }
1581
1582 #[test]
1583 fn test_yaml_custom_key_order_unsorted() {
1584 let rule = create_rule_with_key_order(vec!["title", "date", "author"]);
1586 let content = "---\ntitle: Test\nauthor: John\ndate: 2024-01-01\n---\n\n# Heading";
1587 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1588 let result = rule.check(&ctx).unwrap();
1589
1590 assert_eq!(result.len(), 1);
1591 assert!(result[0].message.contains("'date' should come before 'author'"));
1593 }
1594
1595 #[test]
1596 fn test_yaml_custom_key_order_unlisted_keys_alphabetical() {
1597 let rule = create_rule_with_key_order(vec!["title"]);
1599 let content = "---\ntitle: Test\nauthor: John\ndate: 2024-01-01\n---\n\n# Heading";
1600 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1601 let result = rule.check(&ctx).unwrap();
1602
1603 assert!(result.is_empty());
1606 }
1607
1608 #[test]
1609 fn test_yaml_custom_key_order_unlisted_keys_unsorted() {
1610 let rule = create_rule_with_key_order(vec!["title"]);
1612 let content = "---\ntitle: Test\nzebra: Zoo\nauthor: John\n---\n\n# Heading";
1613 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1614 let result = rule.check(&ctx).unwrap();
1615
1616 assert_eq!(result.len(), 1);
1618 assert!(result[0].message.contains("'author' should come before 'zebra'"));
1619 }
1620
1621 #[test]
1622 fn test_yaml_custom_key_order_fix() {
1623 let rule = create_rule_with_key_order(vec!["title", "date", "author"]);
1624 let content = "---\nauthor: John\ndate: 2024-01-01\ntitle: Test\n---\n\n# Heading";
1625 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1626 let fixed = rule.fix(&ctx).unwrap();
1627
1628 let title_pos = fixed.find("title:").unwrap();
1630 let date_pos = fixed.find("date:").unwrap();
1631 let author_pos = fixed.find("author:").unwrap();
1632 assert!(
1633 title_pos < date_pos && date_pos < author_pos,
1634 "Fixed YAML should have keys in custom order: title, date, author. Got:\n{fixed}"
1635 );
1636 }
1637
1638 #[test]
1639 fn test_yaml_custom_key_order_fix_with_unlisted() {
1640 let rule = create_rule_with_key_order(vec!["title", "author"]);
1642 let content = "---\nzebra: Zoo\nauthor: John\ntitle: Test\naardvark: Ant\n---\n\n# Heading";
1643 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1644 let fixed = rule.fix(&ctx).unwrap();
1645
1646 let title_pos = fixed.find("title:").unwrap();
1648 let author_pos = fixed.find("author:").unwrap();
1649 let aardvark_pos = fixed.find("aardvark:").unwrap();
1650 let zebra_pos = fixed.find("zebra:").unwrap();
1651
1652 assert!(
1653 title_pos < author_pos && author_pos < aardvark_pos && aardvark_pos < zebra_pos,
1654 "Fixed YAML should have specified keys first, then unlisted alphabetically. Got:\n{fixed}"
1655 );
1656 }
1657
1658 #[test]
1659 fn test_toml_custom_key_order_sorted() {
1660 let rule = create_rule_with_key_order(vec!["title", "date", "author"]);
1661 let content = "+++\ntitle = \"Test\"\ndate = \"2024-01-01\"\nauthor = \"John\"\n+++\n\n# Heading";
1662 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1663 let result = rule.check(&ctx).unwrap();
1664
1665 assert!(result.is_empty());
1666 }
1667
1668 #[test]
1669 fn test_toml_custom_key_order_unsorted() {
1670 let rule = create_rule_with_key_order(vec!["title", "date", "author"]);
1671 let content = "+++\nauthor = \"John\"\ntitle = \"Test\"\ndate = \"2024-01-01\"\n+++\n\n# Heading";
1672 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1673 let result = rule.check(&ctx).unwrap();
1674
1675 assert_eq!(result.len(), 1);
1676 assert!(result[0].message.contains("TOML"));
1677 }
1678
1679 #[test]
1680 fn test_json_custom_key_order_sorted() {
1681 let rule = create_rule_with_key_order(vec!["title", "date", "author"]);
1682 let content = "{\n \"title\": \"Test\",\n \"date\": \"2024-01-01\",\n \"author\": \"John\"\n}\n\n# Heading";
1683 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1684 let result = rule.check(&ctx).unwrap();
1685
1686 assert!(result.is_empty());
1687 }
1688
1689 #[test]
1690 fn test_json_custom_key_order_unsorted() {
1691 let rule = create_rule_with_key_order(vec!["title", "date", "author"]);
1692 let content = "{\n \"author\": \"John\",\n \"title\": \"Test\",\n \"date\": \"2024-01-01\"\n}\n\n# Heading";
1693 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1694 let result = rule.check(&ctx).unwrap();
1695
1696 assert_eq!(result.len(), 1);
1697 assert!(result[0].message.contains("JSON"));
1698 }
1699
1700 #[test]
1701 fn test_key_order_case_insensitive_match() {
1702 let rule = create_rule_with_key_order(vec!["Title", "Date", "Author"]);
1704 let content = "---\ntitle: Test\ndate: 2024-01-01\nauthor: John\n---\n\n# Heading";
1705 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1706 let result = rule.check(&ctx).unwrap();
1707
1708 assert!(result.is_empty());
1710 }
1711
1712 #[test]
1713 fn test_key_order_partial_match() {
1714 let rule = create_rule_with_key_order(vec!["title"]);
1716 let content = "---\ntitle: Test\ndate: 2024-01-01\nauthor: John\n---\n\n# Heading";
1717 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1718 let result = rule.check(&ctx).unwrap();
1719
1720 assert_eq!(result.len(), 1);
1731 assert!(result[0].message.contains("'author' should come before 'date'"));
1732 }
1733
1734 #[test]
1737 fn test_key_order_empty_array_falls_back_to_alphabetical() {
1738 let rule = MD072FrontmatterKeySort::from_config_struct(MD072Config {
1740 enabled: true,
1741 key_order: Some(vec![]),
1742 });
1743 let content = "---\ntitle: Test\nauthor: John\n---\n\n# Heading";
1744 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1745 let result = rule.check(&ctx).unwrap();
1746
1747 assert_eq!(result.len(), 1);
1750 assert!(result[0].message.contains("'author' should come before 'title'"));
1751 }
1752
1753 #[test]
1754 fn test_key_order_single_key() {
1755 let rule = create_rule_with_key_order(vec!["title"]);
1757 let content = "---\ntitle: Test\n---\n\n# Heading";
1758 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1759 let result = rule.check(&ctx).unwrap();
1760
1761 assert!(result.is_empty());
1762 }
1763
1764 #[test]
1765 fn test_key_order_all_keys_specified() {
1766 let rule = create_rule_with_key_order(vec!["title", "author", "date"]);
1768 let content = "---\ntitle: Test\nauthor: John\ndate: 2024-01-01\n---\n\n# Heading";
1769 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1770 let result = rule.check(&ctx).unwrap();
1771
1772 assert!(result.is_empty());
1773 }
1774
1775 #[test]
1776 fn test_key_order_no_keys_match() {
1777 let rule = create_rule_with_key_order(vec!["foo", "bar", "baz"]);
1779 let content = "---\nauthor: John\ndate: 2024-01-01\ntitle: Test\n---\n\n# Heading";
1780 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1781 let result = rule.check(&ctx).unwrap();
1782
1783 assert!(result.is_empty());
1786 }
1787
1788 #[test]
1789 fn test_key_order_no_keys_match_unsorted() {
1790 let rule = create_rule_with_key_order(vec!["foo", "bar", "baz"]);
1792 let content = "---\ntitle: Test\ndate: 2024-01-01\nauthor: John\n---\n\n# Heading";
1793 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1794 let result = rule.check(&ctx).unwrap();
1795
1796 assert_eq!(result.len(), 1);
1799 }
1800
1801 #[test]
1802 fn test_key_order_duplicate_keys_in_config() {
1803 let rule = MD072FrontmatterKeySort::from_config_struct(MD072Config {
1805 enabled: true,
1806 key_order: Some(vec![
1807 "title".to_string(),
1808 "author".to_string(),
1809 "title".to_string(), ]),
1811 });
1812 let content = "---\ntitle: Test\nauthor: John\n---\n\n# Heading";
1813 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1814 let result = rule.check(&ctx).unwrap();
1815
1816 assert!(result.is_empty());
1818 }
1819
1820 #[test]
1821 fn test_key_order_with_comments_still_skips_fix() {
1822 let rule = create_rule_with_key_order(vec!["title", "author"]);
1824 let content = "---\n# This is a comment\nauthor: John\ntitle: Test\n---\n\n# Heading";
1825 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1826 let result = rule.check(&ctx).unwrap();
1827
1828 assert_eq!(result.len(), 1);
1830 assert!(result[0].message.contains("auto-fix unavailable"));
1831 assert!(result[0].fix.is_none());
1832 }
1833
1834 #[test]
1835 fn test_toml_custom_key_order_fix() {
1836 let rule = create_rule_with_key_order(vec!["title", "date", "author"]);
1837 let content = "+++\nauthor = \"John\"\ndate = \"2024-01-01\"\ntitle = \"Test\"\n+++\n\n# Heading";
1838 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1839 let fixed = rule.fix(&ctx).unwrap();
1840
1841 let title_pos = fixed.find("title").unwrap();
1843 let date_pos = fixed.find("date").unwrap();
1844 let author_pos = fixed.find("author").unwrap();
1845 assert!(
1846 title_pos < date_pos && date_pos < author_pos,
1847 "Fixed TOML should have keys in custom order. Got:\n{fixed}"
1848 );
1849 }
1850
1851 #[test]
1852 fn test_json_custom_key_order_fix() {
1853 let rule = create_rule_with_key_order(vec!["title", "date", "author"]);
1854 let content = "{\n \"author\": \"John\",\n \"date\": \"2024-01-01\",\n \"title\": \"Test\"\n}\n\n# Heading";
1855 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1856 let fixed = rule.fix(&ctx).unwrap();
1857
1858 let title_pos = fixed.find("\"title\"").unwrap();
1860 let date_pos = fixed.find("\"date\"").unwrap();
1861 let author_pos = fixed.find("\"author\"").unwrap();
1862 assert!(
1863 title_pos < date_pos && date_pos < author_pos,
1864 "Fixed JSON should have keys in custom order. Got:\n{fixed}"
1865 );
1866 }
1867
1868 #[test]
1869 fn test_key_order_unicode_keys() {
1870 let rule = MD072FrontmatterKeySort::from_config_struct(MD072Config {
1872 enabled: true,
1873 key_order: Some(vec!["タイトル".to_string(), "著者".to_string()]),
1874 });
1875 let content = "---\nタイトル: テスト\n著者: 山田太郎\n---\n\n# Heading";
1876 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1877 let result = rule.check(&ctx).unwrap();
1878
1879 assert!(result.is_empty());
1881 }
1882
1883 #[test]
1884 fn test_key_order_mixed_specified_and_unlisted_boundary() {
1885 let rule = create_rule_with_key_order(vec!["z_last_specified"]);
1887 let content = "---\nz_last_specified: value\na_first_unlisted: value\n---\n\n# Heading";
1888 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1889 let result = rule.check(&ctx).unwrap();
1890
1891 assert!(result.is_empty());
1894 }
1895
1896 #[test]
1897 fn test_key_order_fix_preserves_values() {
1898 let rule = create_rule_with_key_order(vec!["title", "tags"]);
1900 let content = "---\ntags:\n - rust\n - markdown\ntitle: Test\n---\n\n# Heading";
1901 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1902 let fixed = rule.fix(&ctx).unwrap();
1903
1904 let title_pos = fixed.find("title:").unwrap();
1906 let tags_pos = fixed.find("tags:").unwrap();
1907 assert!(title_pos < tags_pos, "title should come before tags");
1908
1909 assert!(fixed.contains("- rust"), "List items should be preserved");
1911 assert!(fixed.contains("- markdown"), "List items should be preserved");
1912 }
1913
1914 #[test]
1915 fn test_key_order_idempotent_fix() {
1916 let rule = create_rule_with_key_order(vec!["title", "date", "author"]);
1918 let content = "---\nauthor: John\ndate: 2024-01-01\ntitle: Test\n---\n\n# Heading";
1919 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1920
1921 let fixed_once = rule.fix(&ctx).unwrap();
1922 let ctx2 = LintContext::new(&fixed_once, crate::config::MarkdownFlavor::Standard, None);
1923 let fixed_twice = rule.fix(&ctx2).unwrap();
1924
1925 assert_eq!(fixed_once, fixed_twice, "Fix should be idempotent");
1926 }
1927
1928 #[test]
1929 fn test_key_order_respects_later_position_over_alphabetical() {
1930 let rule = create_rule_with_key_order(vec!["zebra", "aardvark"]);
1932 let content = "---\nzebra: Zoo\naardvark: Ant\n---\n\n# Heading";
1933 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1934 let result = rule.check(&ctx).unwrap();
1935
1936 assert!(result.is_empty());
1938 }
1939
1940 #[test]
1943 fn test_json_braces_in_string_values_extracts_all_keys() {
1944 let rule = create_enabled_rule();
1948 let content = "{\n\"author\": \"Someone\",\n\"description\": \"Use { to open\",\n\"tags\": [\"a\"],\n\"title\": \"My Post\"\n}\n\nContent here.\n";
1949 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1950 let result = rule.check(&ctx).unwrap();
1951
1952 assert!(
1954 result.is_empty(),
1955 "All keys should be extracted and recognized as sorted. Got: {result:?}"
1956 );
1957 }
1958
1959 #[test]
1960 fn test_json_braces_in_string_key_after_brace_value_detected() {
1961 let rule = create_enabled_rule();
1963 let content = "{\n\"description\": \"Use { to open\",\n\"author\": \"Someone\"\n}\n\nContent.\n";
1966 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1967 let result = rule.check(&ctx).unwrap();
1968
1969 assert_eq!(
1972 result.len(),
1973 1,
1974 "Should detect unsorted keys after brace-containing string value"
1975 );
1976 assert!(
1977 result[0].message.contains("'author' should come before 'description'"),
1978 "Should report author before description. Got: {}",
1979 result[0].message
1980 );
1981 }
1982
1983 #[test]
1984 fn test_json_brackets_in_string_values() {
1985 let rule = create_enabled_rule();
1987 let content = "{\n\"description\": \"My [Post]\",\n\"author\": \"Someone\"\n}\n\nContent.\n";
1988 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1989 let result = rule.check(&ctx).unwrap();
1990
1991 assert_eq!(
1993 result.len(),
1994 1,
1995 "Should detect unsorted keys despite brackets in string values"
1996 );
1997 assert!(
1998 result[0].message.contains("'author' should come before 'description'"),
1999 "Got: {}",
2000 result[0].message
2001 );
2002 }
2003
2004 #[test]
2005 fn test_json_escaped_quotes_in_values() {
2006 let rule = create_enabled_rule();
2008 let content = "{\n\"title\": \"He said \\\"hello {world}\\\"\",\n\"author\": \"Someone\"\n}\n\nContent.\n";
2009 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2010 let result = rule.check(&ctx).unwrap();
2011
2012 assert_eq!(result.len(), 1, "Should handle escaped quotes with braces in values");
2014 assert!(
2015 result[0].message.contains("'author' should come before 'title'"),
2016 "Got: {}",
2017 result[0].message
2018 );
2019 }
2020
2021 #[test]
2022 fn test_json_multiple_braces_in_string() {
2023 let rule = create_enabled_rule();
2025 let content = "{\n\"pattern\": \"{{{}}\",\n\"author\": \"Someone\"\n}\n\nContent.\n";
2026 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2027 let result = rule.check(&ctx).unwrap();
2028
2029 assert_eq!(result.len(), 1, "Should handle multiple braces in string values");
2031 assert!(
2032 result[0].message.contains("'author' should come before 'pattern'"),
2033 "Got: {}",
2034 result[0].message
2035 );
2036 }
2037
2038 #[test]
2039 fn test_key_order_detects_wrong_custom_order() {
2040 let rule = create_rule_with_key_order(vec!["zebra", "aardvark"]);
2042 let content = "---\naardvark: Ant\nzebra: Zoo\n---\n\n# Heading";
2043 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2044 let result = rule.check(&ctx).unwrap();
2045
2046 assert_eq!(result.len(), 1);
2047 assert!(result[0].message.contains("'zebra' should come before 'aardvark'"));
2048 }
2049}