1use crate::filtered_lines::FilteredLinesExt;
29use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
30use crate::rule_config_serde::RuleConfig;
31use crate::utils::sentence_utils::is_after_sentence_ending;
32use crate::utils::skip_context::is_table_line;
33use serde::{Deserialize, Serialize};
34use std::sync::Arc;
35
36use regex::Regex;
38use std::sync::LazyLock;
39
40static MULTIPLE_SPACES_REGEX: LazyLock<Regex> = LazyLock::new(|| {
41 Regex::new(r" {2,}").unwrap()
43});
44
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
47#[serde(rename_all = "kebab-case")]
48pub struct MD064Config {
49 #[serde(
73 default = "default_allow_sentence_double_space",
74 alias = "allow_sentence_double_space"
75 )]
76 pub allow_sentence_double_space: bool,
77}
78
79fn default_allow_sentence_double_space() -> bool {
80 false
81}
82
83impl Default for MD064Config {
84 fn default() -> Self {
85 Self {
86 allow_sentence_double_space: default_allow_sentence_double_space(),
87 }
88 }
89}
90
91impl RuleConfig for MD064Config {
92 const RULE_NAME: &'static str = "MD064";
93}
94
95#[derive(Debug, Clone)]
96pub struct MD064NoMultipleConsecutiveSpaces {
97 config: MD064Config,
98}
99
100impl Default for MD064NoMultipleConsecutiveSpaces {
101 fn default() -> Self {
102 Self::new()
103 }
104}
105
106impl MD064NoMultipleConsecutiveSpaces {
107 pub fn new() -> Self {
108 Self {
109 config: MD064Config::default(),
110 }
111 }
112
113 pub fn from_config_struct(config: MD064Config) -> Self {
114 Self { config }
115 }
116
117 fn is_in_code_span(&self, code_spans: &[crate::lint_context::CodeSpan], byte_pos: usize) -> bool {
119 code_spans
120 .iter()
121 .any(|span| byte_pos >= span.byte_offset && byte_pos < span.byte_end)
122 }
123
124 fn is_trailing_whitespace(&self, line: &str, match_end: usize) -> bool {
127 let remaining = &line[match_end..];
129 remaining.is_empty() || remaining.chars().all(|c| c == '\n' || c == '\r')
130 }
131
132 fn is_leading_indentation(&self, line: &str, match_start: usize) -> bool {
134 line[..match_start].chars().all(|c| c == ' ' || c == '\t')
136 }
137
138 fn is_after_list_marker(&self, line: &str, match_start: usize) -> bool {
140 let before = line[..match_start].trim_start();
141
142 if before == "*" || before == "-" || before == "+" {
144 return true;
145 }
146
147 if before.len() >= 2 {
150 let last_char = before.chars().last().unwrap();
151 if last_char == '.' || last_char == ')' {
152 let prefix = &before[..before.len() - 1];
153 if !prefix.is_empty() && prefix.chars().all(|c| c.is_ascii_digit()) {
154 return true;
155 }
156 }
157 }
158
159 false
160 }
161
162 fn is_after_blockquote_marker(&self, line: &str, match_start: usize) -> bool {
165 let before = line[..match_start].trim_start();
166
167 if before.is_empty() {
169 return false;
170 }
171
172 let trimmed = before.trim_end();
174 if trimmed.chars().all(|c| c == '>') {
175 return true;
176 }
177
178 if trimmed.ends_with('>') {
180 let inner = trimmed.trim_end_matches('>').trim();
181 if inner.is_empty() || inner.chars().all(|c| c == '>') {
182 return true;
183 }
184 }
185
186 false
187 }
188
189 fn is_tab_replacement_pattern(&self, space_count: usize) -> bool {
193 space_count >= 4 && space_count.is_multiple_of(4)
194 }
195
196 fn is_reference_link_definition(&self, line: &str, match_start: usize) -> bool {
199 let trimmed = line.trim_start();
200
201 if trimmed.starts_with('[')
203 && let Some(bracket_end) = trimmed.find("]:")
204 {
205 let colon_pos = trimmed.len() - trimmed.trim_start().len() + bracket_end + 2;
206 if match_start >= colon_pos - 1 && match_start <= colon_pos + 1 {
208 return true;
209 }
210 }
211
212 false
213 }
214
215 fn is_after_footnote_marker(&self, line: &str, match_start: usize) -> bool {
218 let trimmed = line.trim_start();
219
220 if trimmed.starts_with("[^")
222 && let Some(bracket_end) = trimmed.find("]:")
223 {
224 let leading_spaces = line.len() - trimmed.len();
225 let colon_pos = leading_spaces + bracket_end + 2;
226 if match_start >= colon_pos.saturating_sub(1) && match_start <= colon_pos + 1 {
228 return true;
229 }
230 }
231
232 false
233 }
234
235 fn is_after_definition_marker(&self, line: &str, match_start: usize) -> bool {
238 let before = line[..match_start].trim_start();
239
240 before == ":"
242 }
243
244 fn is_after_task_checkbox(&self, line: &str, match_start: usize) -> bool {
247 let before = line[..match_start].trim_start();
248
249 if before.len() >= 4 {
252 let patterns = [
253 "- [ ]", "- [x]", "- [X]", "* [ ]", "* [x]", "* [X]", "+ [ ]", "+ [x]", "+ [X]",
254 ];
255 for pattern in patterns {
256 if before == pattern {
257 return true;
258 }
259 }
260 }
261
262 false
263 }
264
265 fn is_table_without_outer_pipes(&self, line: &str) -> bool {
268 let trimmed = line.trim();
269
270 if !trimmed.contains('|') {
272 return false;
273 }
274
275 if trimmed.starts_with('|') || trimmed.ends_with('|') {
277 return false;
278 }
279
280 let parts: Vec<&str> = trimmed.split('|').collect();
284 if parts.len() >= 2 {
285 let first_has_content = !parts.first().unwrap_or(&"").trim().is_empty();
288 let last_has_content = !parts.last().unwrap_or(&"").trim().is_empty();
289 if first_has_content || last_has_content {
290 return true;
291 }
292 }
293
294 false
295 }
296}
297
298impl Rule for MD064NoMultipleConsecutiveSpaces {
299 fn name(&self) -> &'static str {
300 "MD064"
301 }
302
303 fn description(&self) -> &'static str {
304 "Multiple consecutive spaces"
305 }
306
307 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
308 let content = ctx.content;
309
310 if !content.contains(" ") {
312 return Ok(vec![]);
313 }
314
315 let mut warnings = Vec::new();
317 let code_spans: Arc<Vec<crate::lint_context::CodeSpan>> = ctx.code_spans();
318 let line_index = &ctx.line_index;
319
320 for line in ctx
322 .filtered_lines()
323 .skip_front_matter()
324 .skip_code_blocks()
325 .skip_html_blocks()
326 .skip_html_comments()
327 .skip_mkdocstrings()
328 .skip_esm_blocks()
329 {
330 if !line.content.contains(" ") {
332 continue;
333 }
334
335 if is_table_line(line.content) {
337 continue;
338 }
339
340 if self.is_table_without_outer_pipes(line.content) {
342 continue;
343 }
344
345 let line_start_byte = line_index.get_line_start_byte(line.line_num).unwrap_or(0);
346
347 for mat in MULTIPLE_SPACES_REGEX.find_iter(line.content) {
349 let match_start = mat.start();
350 let match_end = mat.end();
351 let space_count = match_end - match_start;
352
353 if self.is_leading_indentation(line.content, match_start) {
355 continue;
356 }
357
358 if self.is_trailing_whitespace(line.content, match_end) {
360 continue;
361 }
362
363 if self.is_tab_replacement_pattern(space_count) {
366 continue;
367 }
368
369 if self.is_after_list_marker(line.content, match_start) {
371 continue;
372 }
373
374 if self.is_after_blockquote_marker(line.content, match_start) {
376 continue;
377 }
378
379 if self.is_after_footnote_marker(line.content, match_start) {
381 continue;
382 }
383
384 if self.is_reference_link_definition(line.content, match_start) {
386 continue;
387 }
388
389 if self.is_after_definition_marker(line.content, match_start) {
391 continue;
392 }
393
394 if self.is_after_task_checkbox(line.content, match_start) {
396 continue;
397 }
398
399 if self.config.allow_sentence_double_space
402 && space_count == 2
403 && is_after_sentence_ending(line.content, match_start)
404 {
405 continue;
406 }
407
408 let abs_byte_start = line_start_byte + match_start;
410
411 if self.is_in_code_span(&code_spans, abs_byte_start) {
413 continue;
414 }
415
416 let abs_byte_end = line_start_byte + match_end;
418
419 let replacement =
422 if self.config.allow_sentence_double_space && is_after_sentence_ending(line.content, match_start) {
423 " ".to_string() } else {
425 " ".to_string() };
427
428 warnings.push(LintWarning {
429 rule_name: Some(self.name().to_string()),
430 message: format!("Multiple consecutive spaces ({space_count}) found"),
431 line: line.line_num,
432 column: match_start + 1, end_line: line.line_num,
434 end_column: match_end + 1, severity: Severity::Warning,
436 fix: Some(Fix {
437 range: abs_byte_start..abs_byte_end,
438 replacement,
439 }),
440 });
441 }
442 }
443
444 Ok(warnings)
445 }
446
447 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
448 let content = ctx.content;
449
450 if !content.contains(" ") {
452 return Ok(content.to_string());
453 }
454
455 let warnings = self.check(ctx)?;
457 if warnings.is_empty() {
458 return Ok(content.to_string());
459 }
460
461 let mut fixes: Vec<(std::ops::Range<usize>, String)> = warnings
463 .into_iter()
464 .filter_map(|w| w.fix.map(|f| (f.range, f.replacement)))
465 .collect();
466
467 fixes.sort_by_key(|(range, _)| std::cmp::Reverse(range.start));
468
469 let mut result = content.to_string();
471 for (range, replacement) in fixes {
472 if range.start < result.len() && range.end <= result.len() {
473 result.replace_range(range, &replacement);
474 }
475 }
476
477 Ok(result)
478 }
479
480 fn category(&self) -> RuleCategory {
482 RuleCategory::Whitespace
483 }
484
485 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
487 ctx.content.is_empty() || !ctx.content.contains(" ")
488 }
489
490 fn as_any(&self) -> &dyn std::any::Any {
491 self
492 }
493
494 fn default_config_section(&self) -> Option<(String, toml::Value)> {
495 let default_config = MD064Config::default();
496 let json_value = serde_json::to_value(&default_config).ok()?;
497 let toml_value = crate::rule_config_serde::json_to_toml_value(&json_value)?;
498
499 if let toml::Value::Table(table) = toml_value {
500 if !table.is_empty() {
501 Some((MD064Config::RULE_NAME.to_string(), toml::Value::Table(table)))
502 } else {
503 None
504 }
505 } else {
506 None
507 }
508 }
509
510 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
511 where
512 Self: Sized,
513 {
514 let rule_config = crate::rule_config_serde::load_rule_config::<MD064Config>(config);
515 Box::new(MD064NoMultipleConsecutiveSpaces::from_config_struct(rule_config))
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522 use crate::lint_context::LintContext;
523
524 #[test]
525 fn test_basic_multiple_spaces() {
526 let rule = MD064NoMultipleConsecutiveSpaces::new();
527
528 let content = "This is a sentence with extra spaces.";
530 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
531 let result = rule.check(&ctx).unwrap();
532 assert_eq!(result.len(), 1);
533 assert_eq!(result[0].line, 1);
534 assert_eq!(result[0].column, 8); }
536
537 #[test]
538 fn test_no_issues_single_spaces() {
539 let rule = MD064NoMultipleConsecutiveSpaces::new();
540
541 let content = "This is a normal sentence with single spaces.";
543 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
544 let result = rule.check(&ctx).unwrap();
545 assert!(result.is_empty());
546 }
547
548 #[test]
549 fn test_skip_inline_code() {
550 let rule = MD064NoMultipleConsecutiveSpaces::new();
551
552 let content = "Use `code with spaces` for formatting.";
554 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
555 let result = rule.check(&ctx).unwrap();
556 assert!(result.is_empty());
557 }
558
559 #[test]
560 fn test_skip_code_blocks() {
561 let rule = MD064NoMultipleConsecutiveSpaces::new();
562
563 let content = "# Heading\n\n```\ncode with spaces\n```\n\nNormal text.";
565 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
566 let result = rule.check(&ctx).unwrap();
567 assert!(result.is_empty());
568 }
569
570 #[test]
571 fn test_skip_leading_indentation() {
572 let rule = MD064NoMultipleConsecutiveSpaces::new();
573
574 let content = " This is indented text.";
576 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
577 let result = rule.check(&ctx).unwrap();
578 assert!(result.is_empty());
579 }
580
581 #[test]
582 fn test_skip_trailing_spaces() {
583 let rule = MD064NoMultipleConsecutiveSpaces::new();
584
585 let content = "Line with trailing spaces \nNext line.";
587 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
588 let result = rule.check(&ctx).unwrap();
589 assert!(result.is_empty());
590 }
591
592 #[test]
593 fn test_skip_all_trailing_spaces() {
594 let rule = MD064NoMultipleConsecutiveSpaces::new();
595
596 let content = "Two spaces \nThree spaces \nFour spaces \n";
598 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
599 let result = rule.check(&ctx).unwrap();
600 assert!(result.is_empty());
601 }
602
603 #[test]
604 fn test_skip_front_matter() {
605 let rule = MD064NoMultipleConsecutiveSpaces::new();
606
607 let content = "---\ntitle: Test Title\n---\n\nContent here.";
609 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
610 let result = rule.check(&ctx).unwrap();
611 assert!(result.is_empty());
612 }
613
614 #[test]
615 fn test_skip_html_comments() {
616 let rule = MD064NoMultipleConsecutiveSpaces::new();
617
618 let content = "<!-- comment with spaces -->\n\nContent here.";
620 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
621 let result = rule.check(&ctx).unwrap();
622 assert!(result.is_empty());
623 }
624
625 #[test]
626 fn test_multiple_issues_one_line() {
627 let rule = MD064NoMultipleConsecutiveSpaces::new();
628
629 let content = "This has multiple issues.";
631 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
632 let result = rule.check(&ctx).unwrap();
633 assert_eq!(result.len(), 3, "Should flag all 3 occurrences");
634 }
635
636 #[test]
637 fn test_fix_collapses_spaces() {
638 let rule = MD064NoMultipleConsecutiveSpaces::new();
639
640 let content = "This is a sentence with extra spaces.";
641 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
642 let fixed = rule.fix(&ctx).unwrap();
643 assert_eq!(fixed, "This is a sentence with extra spaces.");
644 }
645
646 #[test]
647 fn test_fix_preserves_inline_code() {
648 let rule = MD064NoMultipleConsecutiveSpaces::new();
649
650 let content = "Text here `code inside` and more.";
651 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
652 let fixed = rule.fix(&ctx).unwrap();
653 assert_eq!(fixed, "Text here `code inside` and more.");
654 }
655
656 #[test]
657 fn test_fix_preserves_trailing_spaces() {
658 let rule = MD064NoMultipleConsecutiveSpaces::new();
659
660 let content = "Line with extra and trailing \nNext line.";
662 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
663 let fixed = rule.fix(&ctx).unwrap();
664 assert_eq!(fixed, "Line with extra and trailing \nNext line.");
666 }
667
668 #[test]
669 fn test_list_items_with_extra_spaces() {
670 let rule = MD064NoMultipleConsecutiveSpaces::new();
671
672 let content = "- Item one\n- Item two\n";
673 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
674 let result = rule.check(&ctx).unwrap();
675 assert_eq!(result.len(), 2, "Should flag spaces in list items");
676 }
677
678 #[test]
679 fn test_blockquote_with_extra_spaces_in_content() {
680 let rule = MD064NoMultipleConsecutiveSpaces::new();
681
682 let content = "> Quote with extra spaces\n";
684 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
685 let result = rule.check(&ctx).unwrap();
686 assert_eq!(result.len(), 2, "Should flag spaces in blockquote content");
687 }
688
689 #[test]
690 fn test_skip_blockquote_marker_spaces() {
691 let rule = MD064NoMultipleConsecutiveSpaces::new();
692
693 let content = "> Text with extra space after marker\n";
695 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
696 let result = rule.check(&ctx).unwrap();
697 assert!(result.is_empty());
698
699 let content = "> Text with three spaces after marker\n";
701 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
702 let result = rule.check(&ctx).unwrap();
703 assert!(result.is_empty());
704
705 let content = ">> Nested blockquote\n";
707 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
708 let result = rule.check(&ctx).unwrap();
709 assert!(result.is_empty());
710 }
711
712 #[test]
713 fn test_mixed_content() {
714 let rule = MD064NoMultipleConsecutiveSpaces::new();
715
716 let content = r#"# Heading
717
718This has extra spaces.
719
720```
721code here is fine
722```
723
724- List item
725
726> Quote text
727
728Normal paragraph.
729"#;
730 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
731 let result = rule.check(&ctx).unwrap();
732 assert_eq!(result.len(), 3, "Should flag only content outside code blocks");
734 }
735
736 #[test]
737 fn test_multibyte_utf8() {
738 let rule = MD064NoMultipleConsecutiveSpaces::new();
739
740 let content = "日本語 テスト 文字列";
742 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
743 let result = rule.check(&ctx);
744 assert!(result.is_ok(), "Should handle multi-byte UTF-8 characters");
745
746 let warnings = result.unwrap();
747 assert_eq!(warnings.len(), 2, "Should find 2 occurrences of multiple spaces");
748 }
749
750 #[test]
751 fn test_table_rows_skipped() {
752 let rule = MD064NoMultipleConsecutiveSpaces::new();
753
754 let content = "| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |";
756 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
757 let result = rule.check(&ctx).unwrap();
758 assert!(result.is_empty());
760 }
761
762 #[test]
763 fn test_link_text_with_extra_spaces() {
764 let rule = MD064NoMultipleConsecutiveSpaces::new();
765
766 let content = "[Link text](https://example.com)";
768 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
769 let result = rule.check(&ctx).unwrap();
770 assert_eq!(result.len(), 1, "Should flag extra spaces in link text");
771 }
772
773 #[test]
774 fn test_image_alt_with_extra_spaces() {
775 let rule = MD064NoMultipleConsecutiveSpaces::new();
776
777 let content = "";
779 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
780 let result = rule.check(&ctx).unwrap();
781 assert_eq!(result.len(), 1, "Should flag extra spaces in image alt text");
782 }
783
784 #[test]
785 fn test_skip_list_marker_spaces() {
786 let rule = MD064NoMultipleConsecutiveSpaces::new();
787
788 let content = "* Item with extra spaces after marker\n- Another item\n+ Third item\n";
790 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
791 let result = rule.check(&ctx).unwrap();
792 assert!(result.is_empty());
793
794 let content = "1. Item one\n2. Item two\n10. Item ten\n";
796 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
797 let result = rule.check(&ctx).unwrap();
798 assert!(result.is_empty());
799
800 let content = " * Indented item\n 1. Nested numbered item\n";
802 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
803 let result = rule.check(&ctx).unwrap();
804 assert!(result.is_empty());
805 }
806
807 #[test]
808 fn test_flag_spaces_in_list_content() {
809 let rule = MD064NoMultipleConsecutiveSpaces::new();
810
811 let content = "* Item with extra spaces in content\n";
813 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
814 let result = rule.check(&ctx).unwrap();
815 assert_eq!(result.len(), 1, "Should flag extra spaces in list content");
816 }
817
818 #[test]
819 fn test_skip_reference_link_definition_spaces() {
820 let rule = MD064NoMultipleConsecutiveSpaces::new();
821
822 let content = "[ref]: https://example.com\n";
824 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
825 let result = rule.check(&ctx).unwrap();
826 assert!(result.is_empty());
827
828 let content = "[reference-link]: https://example.com \"Title\"\n";
830 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
831 let result = rule.check(&ctx).unwrap();
832 assert!(result.is_empty());
833 }
834
835 #[test]
836 fn test_skip_footnote_marker_spaces() {
837 let rule = MD064NoMultipleConsecutiveSpaces::new();
838
839 let content = "[^1]: Footnote with extra space\n";
841 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
842 let result = rule.check(&ctx).unwrap();
843 assert!(result.is_empty());
844
845 let content = "[^footnote-label]: This is the footnote text.\n";
847 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
848 let result = rule.check(&ctx).unwrap();
849 assert!(result.is_empty());
850 }
851
852 #[test]
853 fn test_skip_definition_list_marker_spaces() {
854 let rule = MD064NoMultipleConsecutiveSpaces::new();
855
856 let content = "Term\n: Definition with extra spaces\n";
858 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
859 let result = rule.check(&ctx).unwrap();
860 assert!(result.is_empty());
861
862 let content = ": Another definition\n";
864 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
865 let result = rule.check(&ctx).unwrap();
866 assert!(result.is_empty());
867 }
868
869 #[test]
870 fn test_skip_task_list_checkbox_spaces() {
871 let rule = MD064NoMultipleConsecutiveSpaces::new();
872
873 let content = "- [ ] Task with extra space\n";
875 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
876 let result = rule.check(&ctx).unwrap();
877 assert!(result.is_empty());
878
879 let content = "- [x] Completed task\n";
881 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
882 let result = rule.check(&ctx).unwrap();
883 assert!(result.is_empty());
884
885 let content = "* [ ] Task with asterisk marker\n";
887 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
888 let result = rule.check(&ctx).unwrap();
889 assert!(result.is_empty());
890 }
891
892 #[test]
893 fn test_skip_table_without_outer_pipes() {
894 let rule = MD064NoMultipleConsecutiveSpaces::new();
895
896 let content = "Col1 | Col2 | Col3\n";
898 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
899 let result = rule.check(&ctx).unwrap();
900 assert!(result.is_empty());
901
902 let content = "--------- | --------- | ---------\n";
904 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
905 let result = rule.check(&ctx).unwrap();
906 assert!(result.is_empty());
907
908 let content = "Data1 | Data2 | Data3\n";
910 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
911 let result = rule.check(&ctx).unwrap();
912 assert!(result.is_empty());
913 }
914
915 #[test]
916 fn test_flag_spaces_in_footnote_content() {
917 let rule = MD064NoMultipleConsecutiveSpaces::new();
918
919 let content = "[^1]: Footnote with extra spaces in content.\n";
921 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
922 let result = rule.check(&ctx).unwrap();
923 assert_eq!(result.len(), 1, "Should flag extra spaces in footnote content");
924 }
925
926 #[test]
927 fn test_flag_spaces_in_reference_content() {
928 let rule = MD064NoMultipleConsecutiveSpaces::new();
929
930 let content = "[ref]: https://example.com \"Title with extra spaces\"\n";
932 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
933 let result = rule.check(&ctx).unwrap();
934 assert_eq!(result.len(), 1, "Should flag extra spaces in reference link title");
935 }
936
937 #[test]
940 fn test_sentence_double_space_disabled_by_default() {
941 let rule = MD064NoMultipleConsecutiveSpaces::new();
943 let content = "First sentence. Second sentence.";
944 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
945 let result = rule.check(&ctx).unwrap();
946 assert_eq!(result.len(), 1, "Default should flag 2 spaces after period");
947 }
948
949 #[test]
950 fn test_sentence_double_space_enabled_allows_period() {
951 let config = MD064Config {
953 allow_sentence_double_space: true,
954 };
955 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
956
957 let content = "First sentence. Second sentence.";
958 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
959 let result = rule.check(&ctx).unwrap();
960 assert!(result.is_empty(), "Should allow 2 spaces after period");
961 }
962
963 #[test]
964 fn test_sentence_double_space_enabled_allows_exclamation() {
965 let config = MD064Config {
966 allow_sentence_double_space: true,
967 };
968 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
969
970 let content = "Wow! That was great.";
971 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
972 let result = rule.check(&ctx).unwrap();
973 assert!(result.is_empty(), "Should allow 2 spaces after exclamation");
974 }
975
976 #[test]
977 fn test_sentence_double_space_enabled_allows_question() {
978 let config = MD064Config {
979 allow_sentence_double_space: true,
980 };
981 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
982
983 let content = "Is this OK? Yes it is.";
984 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
985 let result = rule.check(&ctx).unwrap();
986 assert!(result.is_empty(), "Should allow 2 spaces after question mark");
987 }
988
989 #[test]
990 fn test_sentence_double_space_flags_mid_sentence() {
991 let config = MD064Config {
993 allow_sentence_double_space: true,
994 };
995 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
996
997 let content = "Word word in the middle.";
998 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
999 let result = rule.check(&ctx).unwrap();
1000 assert_eq!(result.len(), 1, "Should flag 2 spaces mid-sentence");
1001 }
1002
1003 #[test]
1004 fn test_sentence_double_space_flags_triple_after_period() {
1005 let config = MD064Config {
1007 allow_sentence_double_space: true,
1008 };
1009 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
1010
1011 let content = "First sentence. Three spaces here.";
1012 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1013 let result = rule.check(&ctx).unwrap();
1014 assert_eq!(result.len(), 1, "Should flag 3 spaces even after period");
1015 }
1016
1017 #[test]
1018 fn test_sentence_double_space_with_closing_quote() {
1019 let config = MD064Config {
1021 allow_sentence_double_space: true,
1022 };
1023 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
1024
1025 let content = r#"He said "Hello." Then he left."#;
1026 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1027 let result = rule.check(&ctx).unwrap();
1028 assert!(result.is_empty(), "Should allow 2 spaces after .\" ");
1029
1030 let content = "She said 'Goodbye.' And she was gone.";
1032 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1033 let result = rule.check(&ctx).unwrap();
1034 assert!(result.is_empty(), "Should allow 2 spaces after .' ");
1035 }
1036
1037 #[test]
1038 fn test_sentence_double_space_with_curly_quotes() {
1039 let config = MD064Config {
1040 allow_sentence_double_space: true,
1041 };
1042 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
1043
1044 let content = format!(
1047 "He said {}Hello.{} Then left.",
1048 '\u{201C}', '\u{201D}' );
1051 let ctx = LintContext::new(&content, crate::config::MarkdownFlavor::Standard, None);
1052 let result = rule.check(&ctx).unwrap();
1053 assert!(result.is_empty(), "Should allow 2 spaces after curly double quote");
1054
1055 let content = format!(
1057 "She said {}Hi.{} And left.",
1058 '\u{2018}', '\u{2019}' );
1061 let ctx = LintContext::new(&content, crate::config::MarkdownFlavor::Standard, None);
1062 let result = rule.check(&ctx).unwrap();
1063 assert!(result.is_empty(), "Should allow 2 spaces after curly single quote");
1064 }
1065
1066 #[test]
1067 fn test_sentence_double_space_with_closing_paren() {
1068 let config = MD064Config {
1069 allow_sentence_double_space: true,
1070 };
1071 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
1072
1073 let content = "(See reference.) The next point is.";
1074 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1075 let result = rule.check(&ctx).unwrap();
1076 assert!(result.is_empty(), "Should allow 2 spaces after .) ");
1077 }
1078
1079 #[test]
1080 fn test_sentence_double_space_with_closing_bracket() {
1081 let config = MD064Config {
1082 allow_sentence_double_space: true,
1083 };
1084 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
1085
1086 let content = "[Citation needed.] More text here.";
1087 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1088 let result = rule.check(&ctx).unwrap();
1089 assert!(result.is_empty(), "Should allow 2 spaces after .] ");
1090 }
1091
1092 #[test]
1093 fn test_sentence_double_space_with_ellipsis() {
1094 let config = MD064Config {
1095 allow_sentence_double_space: true,
1096 };
1097 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
1098
1099 let content = "He paused... Then continued.";
1100 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1101 let result = rule.check(&ctx).unwrap();
1102 assert!(result.is_empty(), "Should allow 2 spaces after ellipsis");
1103 }
1104
1105 #[test]
1106 fn test_sentence_double_space_complex_ending() {
1107 let config = MD064Config {
1109 allow_sentence_double_space: true,
1110 };
1111 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
1112
1113 let content = r#"(He said "Yes.") Then they agreed."#;
1114 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1115 let result = rule.check(&ctx).unwrap();
1116 assert!(result.is_empty(), "Should allow 2 spaces after .\") ");
1117 }
1118
1119 #[test]
1120 fn test_sentence_double_space_mixed_content() {
1121 let config = MD064Config {
1123 allow_sentence_double_space: true,
1124 };
1125 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
1126
1127 let content = "Good sentence. Bad mid-sentence. Another good one! OK? Yes.";
1128 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1129 let result = rule.check(&ctx).unwrap();
1130 assert_eq!(result.len(), 1, "Should only flag mid-sentence double space");
1131 assert!(
1132 result[0].column > 15 && result[0].column < 25,
1133 "Should flag the 'Bad mid' double space"
1134 );
1135 }
1136
1137 #[test]
1138 fn test_sentence_double_space_fix_collapses_to_two() {
1139 let config = MD064Config {
1141 allow_sentence_double_space: true,
1142 };
1143 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
1144
1145 let content = "Sentence. Three spaces here.";
1146 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1147 let fixed = rule.fix(&ctx).unwrap();
1148 assert_eq!(
1149 fixed, "Sentence. Three spaces here.",
1150 "Should collapse to 2 spaces after sentence"
1151 );
1152 }
1153
1154 #[test]
1155 fn test_sentence_double_space_fix_collapses_mid_sentence_to_one() {
1156 let config = MD064Config {
1158 allow_sentence_double_space: true,
1159 };
1160 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
1161
1162 let content = "Word word here.";
1163 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1164 let fixed = rule.fix(&ctx).unwrap();
1165 assert_eq!(fixed, "Word word here.", "Should collapse to 1 space mid-sentence");
1166 }
1167
1168 #[test]
1169 fn test_sentence_double_space_config_kebab_case() {
1170 let toml_str = r#"
1171 allow-sentence-double-space = true
1172 "#;
1173 let config: MD064Config = toml::from_str(toml_str).unwrap();
1174 assert!(config.allow_sentence_double_space);
1175 }
1176
1177 #[test]
1178 fn test_sentence_double_space_config_snake_case() {
1179 let toml_str = r#"
1180 allow_sentence_double_space = true
1181 "#;
1182 let config: MD064Config = toml::from_str(toml_str).unwrap();
1183 assert!(config.allow_sentence_double_space);
1184 }
1185
1186 #[test]
1187 fn test_sentence_double_space_at_line_start() {
1188 let config = MD064Config {
1190 allow_sentence_double_space: true,
1191 };
1192 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
1193
1194 let content = ". Text after period at start.";
1196 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1197 let _result = rule.check(&ctx).unwrap();
1199 }
1200
1201 #[test]
1202 fn test_sentence_double_space_guillemets() {
1203 let config = MD064Config {
1205 allow_sentence_double_space: true,
1206 };
1207 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
1208
1209 let content = "Il a dit «Oui.» Puis il est parti.";
1210 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1211 let result = rule.check(&ctx).unwrap();
1212 assert!(result.is_empty(), "Should allow 2 spaces after .» (guillemet)");
1213 }
1214
1215 #[test]
1216 fn test_sentence_double_space_multiple_sentences() {
1217 let config = MD064Config {
1219 allow_sentence_double_space: true,
1220 };
1221 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
1222
1223 let content = "First. Second. Third. Fourth.";
1224 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1225 let result = rule.check(&ctx).unwrap();
1226 assert!(result.is_empty(), "Should allow all sentence-ending double spaces");
1227 }
1228
1229 #[test]
1230 fn test_sentence_double_space_abbreviation_detection() {
1231 let config = MD064Config {
1233 allow_sentence_double_space: true,
1234 };
1235 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
1236
1237 let content = "Dr. Smith arrived.";
1239 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1240 let result = rule.check(&ctx).unwrap();
1241 assert_eq!(result.len(), 1, "Should flag Dr. as abbreviation, not sentence ending");
1242
1243 let content = "Prof. Williams teaches.";
1245 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1246 let result = rule.check(&ctx).unwrap();
1247 assert_eq!(result.len(), 1, "Should flag Prof. as abbreviation");
1248
1249 let content = "Use e.g. this example.";
1251 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1252 let result = rule.check(&ctx).unwrap();
1253 assert_eq!(result.len(), 1, "Should flag e.g. as abbreviation");
1254
1255 let content = "Acme Inc. Next company.";
1258 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1259 let result = rule.check(&ctx).unwrap();
1260 assert!(
1261 result.is_empty(),
1262 "Inc. not in abbreviation list, treated as sentence end"
1263 );
1264 }
1265
1266 #[test]
1267 fn test_sentence_double_space_default_config_has_correct_defaults() {
1268 let config = MD064Config::default();
1269 assert!(
1270 !config.allow_sentence_double_space,
1271 "Default allow_sentence_double_space should be false"
1272 );
1273 }
1274
1275 #[test]
1276 fn test_sentence_double_space_from_config_integration() {
1277 use crate::config::Config;
1278 use std::collections::BTreeMap;
1279
1280 let mut config = Config::default();
1281 let mut values = BTreeMap::new();
1282 values.insert("allow-sentence-double-space".to_string(), toml::Value::Boolean(true));
1283 config.rules.insert(
1284 "MD064".to_string(),
1285 crate::config::RuleConfig { severity: None, values },
1286 );
1287
1288 let rule = MD064NoMultipleConsecutiveSpaces::from_config(&config);
1289
1290 let content = "Sentence. Two spaces OK. But three is not.";
1292 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1293 let result = rule.check(&ctx).unwrap();
1294 assert_eq!(result.len(), 1, "Should only flag the triple spaces");
1295 }
1296
1297 #[test]
1298 fn test_sentence_double_space_after_inline_code() {
1299 let config = MD064Config {
1301 allow_sentence_double_space: true,
1302 };
1303 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
1304
1305 let content = "Hello from `backticks`. How's it going?";
1307 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1308 let result = rule.check(&ctx).unwrap();
1309 assert!(
1310 result.is_empty(),
1311 "Should allow 2 spaces after inline code ending with period"
1312 );
1313
1314 let content = "Use `foo` and `bar`. Next sentence.";
1316 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1317 let result = rule.check(&ctx).unwrap();
1318 assert!(result.is_empty(), "Should allow 2 spaces after code at end of sentence");
1319
1320 let content = "The `code` worked! Celebrate.";
1322 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1323 let result = rule.check(&ctx).unwrap();
1324 assert!(result.is_empty(), "Should allow 2 spaces after code with exclamation");
1325
1326 let content = "Is `null` falsy? Yes.";
1328 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1329 let result = rule.check(&ctx).unwrap();
1330 assert!(result.is_empty(), "Should allow 2 spaces after code with question mark");
1331
1332 let content = "The `code` is here.";
1334 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1335 let result = rule.check(&ctx).unwrap();
1336 assert_eq!(result.len(), 1, "Should flag 2 spaces after code mid-sentence");
1337 }
1338
1339 #[test]
1340 fn test_sentence_double_space_code_with_closing_punctuation() {
1341 let config = MD064Config {
1343 allow_sentence_double_space: true,
1344 };
1345 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
1346
1347 let content = "(see `example`). Next sentence.";
1349 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1350 let result = rule.check(&ctx).unwrap();
1351 assert!(result.is_empty(), "Should allow 2 spaces after code in parentheses");
1352
1353 let content = "He said \"use `code`\". Then left.";
1355 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1356 let result = rule.check(&ctx).unwrap();
1357 assert!(result.is_empty(), "Should allow 2 spaces after code in quotes");
1358 }
1359
1360 #[test]
1361 fn test_sentence_double_space_after_emphasis() {
1362 let config = MD064Config {
1364 allow_sentence_double_space: true,
1365 };
1366 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
1367
1368 let content = "The word is *important*. Next sentence.";
1370 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1371 let result = rule.check(&ctx).unwrap();
1372 assert!(result.is_empty(), "Should allow 2 spaces after emphasis");
1373
1374 let content = "The word is _important_. Next sentence.";
1376 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1377 let result = rule.check(&ctx).unwrap();
1378 assert!(result.is_empty(), "Should allow 2 spaces after underscore emphasis");
1379
1380 let content = "The word is **critical**. Next sentence.";
1382 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1383 let result = rule.check(&ctx).unwrap();
1384 assert!(result.is_empty(), "Should allow 2 spaces after bold");
1385
1386 let content = "The word is __critical__. Next sentence.";
1388 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1389 let result = rule.check(&ctx).unwrap();
1390 assert!(result.is_empty(), "Should allow 2 spaces after underscore bold");
1391 }
1392
1393 #[test]
1394 fn test_sentence_double_space_after_strikethrough() {
1395 let config = MD064Config {
1397 allow_sentence_double_space: true,
1398 };
1399 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
1400
1401 let content = "This is ~~wrong~~. Next sentence.";
1402 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1403 let result = rule.check(&ctx).unwrap();
1404 assert!(result.is_empty(), "Should allow 2 spaces after strikethrough");
1405
1406 let content = "That was ~~bad~~! Learn from it.";
1408 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1409 let result = rule.check(&ctx).unwrap();
1410 assert!(
1411 result.is_empty(),
1412 "Should allow 2 spaces after strikethrough with exclamation"
1413 );
1414 }
1415
1416 #[test]
1417 fn test_sentence_double_space_after_extended_markdown() {
1418 let config = MD064Config {
1420 allow_sentence_double_space: true,
1421 };
1422 let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
1423
1424 let content = "This is ==highlighted==. Next sentence.";
1426 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1427 let result = rule.check(&ctx).unwrap();
1428 assert!(result.is_empty(), "Should allow 2 spaces after highlight");
1429
1430 let content = "E equals mc^2^. Einstein said.";
1432 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1433 let result = rule.check(&ctx).unwrap();
1434 assert!(result.is_empty(), "Should allow 2 spaces after superscript");
1435 }
1436
1437 #[test]
1438 fn test_inline_config_allow_sentence_double_space() {
1439 let rule = MD064NoMultipleConsecutiveSpaces::new(); let content = "`<svg>`. Fortunately";
1446 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1447 let result = rule.check(&ctx).unwrap();
1448 assert_eq!(result.len(), 1, "Default config should flag double spaces");
1449
1450 let content = r#"<!-- rumdl-configure-file { "MD064": { "allow-sentence-double-space": true } } -->
1453
1454`<svg>`. Fortunately"#;
1455 let inline_config = crate::inline_config::InlineConfig::from_content(content);
1456 let base_config = crate::config::Config::default();
1457 let merged_config = base_config.merge_with_inline_config(&inline_config);
1458 let effective_rule = MD064NoMultipleConsecutiveSpaces::from_config(&merged_config);
1459 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1460 let result = effective_rule.check(&ctx).unwrap();
1461 assert!(
1462 result.is_empty(),
1463 "Inline config should allow double spaces after sentence"
1464 );
1465
1466 let content = r#"<!-- markdownlint-configure-file { "MD064": { "allow-sentence-double-space": true } } -->
1468
1469**scalable**. Pick"#;
1470 let inline_config = crate::inline_config::InlineConfig::from_content(content);
1471 let merged_config = base_config.merge_with_inline_config(&inline_config);
1472 let effective_rule = MD064NoMultipleConsecutiveSpaces::from_config(&merged_config);
1473 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1474 let result = effective_rule.check(&ctx).unwrap();
1475 assert!(result.is_empty(), "Inline config with markdownlint prefix should work");
1476 }
1477
1478 #[test]
1479 fn test_inline_config_allow_sentence_double_space_issue_364() {
1480 let content = r#"<!-- rumdl-configure-file { "MD064": { "allow-sentence-double-space": true } } -->
1484
1485# Title
1486
1487what the font size is for the toplevel `<svg>`. Fortunately, librsvg
1488
1489And here is where I want to say, SVG documents are **scalable**. Pick
1490
1491That's right, no `width`, no `height`, no `viewBox`. There is no easy
1492
1493**SVG documents are scalable**. That's their whole reason for being!"#;
1494
1495 let inline_config = crate::inline_config::InlineConfig::from_content(content);
1497 let base_config = crate::config::Config::default();
1498 let merged_config = base_config.merge_with_inline_config(&inline_config);
1499 let effective_rule = MD064NoMultipleConsecutiveSpaces::from_config(&merged_config);
1500 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1501 let result = effective_rule.check(&ctx).unwrap();
1502 assert!(
1503 result.is_empty(),
1504 "Issue #364: All sentence-ending double spaces should be allowed with inline config. Found {} warnings",
1505 result.len()
1506 );
1507 }
1508}