rumdl_lib/rules/
md030_list_marker_space.rs

1//!
2//! Rule MD030: Spaces after list markers
3//!
4//! See [docs/md030.md](../../docs/md030.md) for full documentation, configuration, and examples.
5
6use crate::rule::{LintResult, LintWarning, Rule, RuleCategory, Severity};
7use crate::rule_config_serde::RuleConfig;
8use crate::rules::list_utils::ListType;
9use crate::utils::element_cache::ElementCache;
10use crate::utils::range_utils::calculate_match_range;
11use toml;
12
13mod md030_config;
14use md030_config::MD030Config;
15
16#[derive(Clone, Default)]
17pub struct MD030ListMarkerSpace {
18    config: MD030Config,
19}
20
21impl MD030ListMarkerSpace {
22    pub fn new(ul_single: usize, ul_multi: usize, ol_single: usize, ol_multi: usize) -> Self {
23        Self {
24            config: MD030Config {
25                ul_single: crate::types::PositiveUsize::new(ul_single)
26                    .unwrap_or(crate::types::PositiveUsize::from_const(1)),
27                ul_multi: crate::types::PositiveUsize::new(ul_multi)
28                    .unwrap_or(crate::types::PositiveUsize::from_const(1)),
29                ol_single: crate::types::PositiveUsize::new(ol_single)
30                    .unwrap_or(crate::types::PositiveUsize::from_const(1)),
31                ol_multi: crate::types::PositiveUsize::new(ol_multi)
32                    .unwrap_or(crate::types::PositiveUsize::from_const(1)),
33            },
34        }
35    }
36
37    pub fn from_config_struct(config: MD030Config) -> Self {
38        Self { config }
39    }
40
41    pub fn get_expected_spaces(&self, list_type: ListType, is_multi: bool) -> usize {
42        match (list_type, is_multi) {
43            (ListType::Unordered, false) => self.config.ul_single.get(),
44            (ListType::Unordered, true) => self.config.ul_multi.get(),
45            (ListType::Ordered, false) => self.config.ol_single.get(),
46            (ListType::Ordered, true) => self.config.ol_multi.get(),
47        }
48    }
49}
50
51impl Rule for MD030ListMarkerSpace {
52    fn name(&self) -> &'static str {
53        "MD030"
54    }
55
56    fn description(&self) -> &'static str {
57        "Spaces after list markers should be consistent"
58    }
59
60    fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
61        let mut warnings = Vec::new();
62
63        // Early return if no list content
64        if self.should_skip(ctx) {
65            return Ok(warnings);
66        }
67
68        // Collect lines once
69        let lines: Vec<&str> = ctx.content.lines().collect();
70
71        // Track which lines we've already processed (to avoid duplicates)
72        let mut processed_lines = std::collections::HashSet::new();
73
74        // First pass: Check parser-recognized list items
75        for (line_num, line_info) in ctx.lines.iter().enumerate() {
76            // Skip code blocks and math blocks - content inside these is not markdown
77            if line_info.list_item.is_some() && !line_info.in_code_block && !line_info.in_math_block {
78                let line_num_1based = line_num + 1;
79                processed_lines.insert(line_num_1based);
80
81                let line = lines[line_num];
82
83                // Skip indented code blocks (4+ columns accounting for tab expansion)
84                if ElementCache::calculate_indentation_width_default(line) >= 4 {
85                    continue;
86                }
87
88                if let Some(list_info) = &line_info.list_item {
89                    let list_type = if list_info.is_ordered {
90                        ListType::Ordered
91                    } else {
92                        ListType::Unordered
93                    };
94
95                    // Calculate actual spacing after marker
96                    let marker_end = list_info.marker_column + list_info.marker.len();
97                    let actual_spaces = list_info.content_column.saturating_sub(marker_end);
98
99                    // Determine if this is a multi-line list item
100                    let is_multi_line = self.is_multi_line_list_item(ctx, line_num_1based, &lines);
101                    let expected_spaces = self.get_expected_spaces(list_type, is_multi_line);
102
103                    if actual_spaces != expected_spaces {
104                        let whitespace_start_pos = marker_end;
105                        let whitespace_len = actual_spaces;
106
107                        let (start_line, start_col, end_line, end_col) =
108                            calculate_match_range(line_num_1based, line, whitespace_start_pos, whitespace_len);
109
110                        let correct_spaces = " ".repeat(expected_spaces);
111                        let line_start_byte = ctx.line_offsets.get(line_num).copied().unwrap_or(0);
112                        let whitespace_start_byte = line_start_byte + whitespace_start_pos;
113                        let whitespace_end_byte = whitespace_start_byte + whitespace_len;
114
115                        let fix = Some(crate::rule::Fix {
116                            range: whitespace_start_byte..whitespace_end_byte,
117                            replacement: correct_spaces,
118                        });
119
120                        let message =
121                            format!("Spaces after list markers (Expected: {expected_spaces}; Actual: {actual_spaces})");
122
123                        warnings.push(LintWarning {
124                            rule_name: Some(self.name().to_string()),
125                            severity: Severity::Warning,
126                            line: start_line,
127                            column: start_col,
128                            end_line,
129                            end_column: end_col,
130                            message,
131                            fix,
132                        });
133                    }
134                }
135            }
136        }
137
138        // Second pass: Detect list-like patterns the parser didn't recognize
139        // This handles cases like "1.Text" where there's no space after the marker
140        for (line_idx, line) in lines.iter().enumerate() {
141            let line_num = line_idx + 1;
142
143            // Skip if already processed or in code block/front matter/math block
144            if processed_lines.contains(&line_num) {
145                continue;
146            }
147            if let Some(line_info) = ctx.lines.get(line_idx)
148                && (line_info.in_code_block
149                    || line_info.in_front_matter
150                    || line_info.in_html_comment
151                    || line_info.in_math_block)
152            {
153                continue;
154            }
155
156            // Skip indented code blocks
157            if self.is_indented_code_block(line, line_idx, &lines) {
158                continue;
159            }
160
161            // Try to detect list-like patterns using regex-based detection
162            if let Some(warning) = self.check_unrecognized_list_marker(ctx, line, line_num, &lines) {
163                warnings.push(warning);
164            }
165        }
166
167        Ok(warnings)
168    }
169
170    fn category(&self) -> RuleCategory {
171        RuleCategory::List
172    }
173
174    fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
175        if ctx.content.is_empty() {
176            return true;
177        }
178
179        // Fast byte-level check for list markers (including ordered lists)
180        let bytes = ctx.content.as_bytes();
181        !bytes.contains(&b'*')
182            && !bytes.contains(&b'-')
183            && !bytes.contains(&b'+')
184            && !bytes.iter().any(|&b| b.is_ascii_digit())
185    }
186
187    fn as_any(&self) -> &dyn std::any::Any {
188        self
189    }
190
191    fn default_config_section(&self) -> Option<(String, toml::Value)> {
192        let default_config = MD030Config::default();
193        let json_value = serde_json::to_value(&default_config).ok()?;
194        let toml_value = crate::rule_config_serde::json_to_toml_value(&json_value)?;
195
196        if let toml::Value::Table(table) = toml_value {
197            if !table.is_empty() {
198                Some((MD030Config::RULE_NAME.to_string(), toml::Value::Table(table)))
199            } else {
200                None
201            }
202        } else {
203            None
204        }
205    }
206
207    fn from_config(config: &crate::config::Config) -> Box<dyn Rule> {
208        let rule_config = crate::rule_config_serde::load_rule_config::<MD030Config>(config);
209        Box::new(Self::from_config_struct(rule_config))
210    }
211
212    fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, crate::rule::LintError> {
213        let content = ctx.content;
214
215        // Early return if no fixes needed
216        if self.should_skip(ctx) {
217            return Ok(content.to_string());
218        }
219
220        let lines: Vec<&str> = content.lines().collect();
221        let mut result_lines = Vec::with_capacity(lines.len());
222
223        for (line_idx, line) in lines.iter().enumerate() {
224            let line_num = line_idx + 1;
225
226            // Skip lines in code blocks, front matter, or HTML comments
227            if let Some(line_info) = ctx.lines.get(line_idx)
228                && (line_info.in_code_block || line_info.in_front_matter || line_info.in_html_comment)
229            {
230                result_lines.push(line.to_string());
231                continue;
232            }
233
234            // Skip if this is an indented code block (4+ spaces with blank line before)
235            if self.is_indented_code_block(line, line_idx, &lines) {
236                result_lines.push(line.to_string());
237                continue;
238            }
239
240            // Use regex-based detection to find list markers, not parser detection.
241            // This ensures we fix spacing on ALL lines that look like list items,
242            // even if the parser doesn't recognize them due to strict nesting rules.
243            // User intention matters: if it looks like a list item, fix it.
244            let is_multi_line = self.is_multi_line_list_item(ctx, line_num, &lines);
245            if let Some(fixed_line) = self.try_fix_list_marker_spacing_with_context(line, is_multi_line) {
246                result_lines.push(fixed_line);
247            } else {
248                result_lines.push(line.to_string());
249            }
250        }
251
252        // Preserve trailing newline if original content had one
253        let result = result_lines.join("\n");
254        if content.ends_with('\n') && !result.ends_with('\n') {
255            Ok(result + "\n")
256        } else {
257            Ok(result)
258        }
259    }
260}
261
262impl MD030ListMarkerSpace {
263    /// Check if a list item is multi-line (spans multiple lines or contains nested content)
264    fn is_multi_line_list_item(&self, ctx: &crate::lint_context::LintContext, line_num: usize, lines: &[&str]) -> bool {
265        // Get the current list item info
266        let current_line_info = match ctx.line_info(line_num) {
267            Some(info) if info.list_item.is_some() => info,
268            _ => return false,
269        };
270
271        let current_list = current_line_info.list_item.as_ref().unwrap();
272
273        // Check subsequent lines to see if they are continuation of this list item
274        for next_line_num in (line_num + 1)..=lines.len() {
275            if let Some(next_line_info) = ctx.line_info(next_line_num) {
276                // If we encounter another list item at the same or higher level, this item is done
277                if let Some(next_list) = &next_line_info.list_item {
278                    if next_list.marker_column <= current_list.marker_column {
279                        break; // Found the next list item at same/higher level
280                    }
281                    // If there's a nested list item, this is multi-line
282                    return true;
283                }
284
285                // If we encounter a non-empty line that's not indented enough to be part of this list item,
286                // this list item is done
287                let line_content = lines.get(next_line_num - 1).unwrap_or(&"");
288                if !line_content.trim().is_empty() {
289                    let expected_continuation_indent = current_list.content_column;
290                    let actual_indent = line_content.len() - line_content.trim_start().len();
291
292                    if actual_indent < expected_continuation_indent {
293                        break; // Line is not indented enough to be part of this list item
294                    }
295
296                    // If we find a continuation line, this is multi-line
297                    if actual_indent >= expected_continuation_indent {
298                        return true;
299                    }
300                }
301
302                // Empty lines don't affect the multi-line status by themselves
303            }
304        }
305
306        false
307    }
308
309    /// Helper to fix marker spacing for both ordered and unordered lists
310    fn fix_marker_spacing(
311        &self,
312        marker: &str,
313        after_marker: &str,
314        indent: &str,
315        is_multi_line: bool,
316        is_ordered: bool,
317    ) -> Option<String> {
318        // MD030 only fixes multiple spaces, not tabs
319        // Tabs are handled by MD010 (no-hard-tabs), matching markdownlint behavior
320        // Skip if the spacing starts with a tab
321        if after_marker.starts_with('\t') {
322            return None;
323        }
324
325        // Calculate expected spacing based on list type and context
326        let expected_spaces = if is_ordered {
327            if is_multi_line {
328                self.config.ol_multi.get()
329            } else {
330                self.config.ol_single.get()
331            }
332        } else if is_multi_line {
333            self.config.ul_multi.get()
334        } else {
335            self.config.ul_single.get()
336        };
337
338        // Case 1: No space after marker (content directly follows marker)
339        // User intention: they meant to write a list item but forgot the space
340        if !after_marker.is_empty() && !after_marker.starts_with(' ') {
341            let spaces = " ".repeat(expected_spaces);
342            return Some(format!("{indent}{marker}{spaces}{after_marker}"));
343        }
344
345        // Case 2: Multiple spaces after marker
346        if after_marker.starts_with("  ") {
347            let content = after_marker.trim_start_matches(' ');
348            if !content.is_empty() {
349                let spaces = " ".repeat(expected_spaces);
350                return Some(format!("{indent}{marker}{spaces}{content}"));
351            }
352        }
353
354        None
355    }
356
357    /// Fix list marker spacing with context - handles tabs, multiple spaces, and mixed whitespace
358    fn try_fix_list_marker_spacing_with_context(&self, line: &str, is_multi_line: bool) -> Option<String> {
359        // Extract blockquote prefix if present
360        let (blockquote_prefix, content) = Self::strip_blockquote_prefix(line);
361
362        let trimmed = content.trim_start();
363        let indent = &content[..content.len() - trimmed.len()];
364
365        // Check for unordered list markers - only fix multiple-space issues, not missing-space
366        // Unordered markers (*, -, +) have too many non-list uses to apply heuristic fixing
367        for marker in &["*", "-", "+"] {
368            if let Some(after_marker) = trimmed.strip_prefix(marker) {
369                // Skip emphasis patterns (**, --, ++)
370                if after_marker.starts_with(*marker) {
371                    break;
372                }
373
374                // Skip if this looks like emphasis: *text* or _text_
375                if *marker == "*" && after_marker.contains('*') {
376                    break;
377                }
378
379                // Only fix if there's already a space (fixing multiple spaces to single space)
380                // Don't add spaces where there are none - too ambiguous for unordered markers
381                if after_marker.starts_with("  ")
382                    && let Some(fixed) = self.fix_marker_spacing(marker, after_marker, indent, is_multi_line, false)
383                {
384                    return Some(format!("{blockquote_prefix}{fixed}"));
385                }
386                break; // Found a marker, don't check others
387            }
388        }
389
390        // Check for ordered list markers
391        if let Some(dot_pos) = trimmed.find('.') {
392            let before_dot = &trimmed[..dot_pos];
393            if before_dot.chars().all(|c| c.is_ascii_digit()) && !before_dot.is_empty() {
394                let after_dot = &trimmed[dot_pos + 1..];
395
396                // Skip empty items
397                if after_dot.is_empty() {
398                    return None;
399                }
400
401                // For NO-SPACE case (content directly after dot), apply "clear user intent" filter
402                if !after_dot.starts_with(' ') && !after_dot.starts_with('\t') {
403                    let first_char = after_dot.chars().next().unwrap_or(' ');
404
405                    // Skip decimal numbers: 3.14, 2.5, etc.
406                    if first_char.is_ascii_digit() {
407                        return None;
408                    }
409
410                    // For CLEAR user intent, only fix if:
411                    // 1. Starts with uppercase letter (strong list indicator), OR
412                    // 2. Starts with [ or ( (link/paren content)
413                    let is_clear_intent = first_char.is_ascii_uppercase() || first_char == '[' || first_char == '(';
414
415                    if !is_clear_intent {
416                        return None;
417                    }
418                }
419                // For items with spaces (including multiple spaces), always let fix_marker_spacing handle it
420
421                let marker = format!("{before_dot}.");
422                if let Some(fixed) = self.fix_marker_spacing(&marker, after_dot, indent, is_multi_line, true) {
423                    return Some(format!("{blockquote_prefix}{fixed}"));
424                }
425            }
426        }
427
428        None
429    }
430
431    /// Strip blockquote prefix from a line, returning (prefix, content)
432    fn strip_blockquote_prefix(line: &str) -> (String, &str) {
433        let mut prefix = String::new();
434        let mut remaining = line;
435
436        loop {
437            let trimmed = remaining.trim_start();
438            if !trimmed.starts_with('>') {
439                break;
440            }
441            // Add leading spaces to prefix
442            let leading_spaces = remaining.len() - trimmed.len();
443            prefix.push_str(&remaining[..leading_spaces]);
444            prefix.push('>');
445            remaining = &trimmed[1..];
446
447            // Handle optional space after >
448            if remaining.starts_with(' ') {
449                prefix.push(' ');
450                remaining = &remaining[1..];
451            }
452        }
453
454        (prefix, remaining)
455    }
456
457    /// Detect list-like patterns that the parser didn't recognize (e.g., "1.Text" with no space)
458    /// This implements user-intention-based detection: if it looks like a list item, flag it
459    fn check_unrecognized_list_marker(
460        &self,
461        ctx: &crate::lint_context::LintContext,
462        line: &str,
463        line_num: usize,
464        lines: &[&str],
465    ) -> Option<LintWarning> {
466        // Strip blockquote prefix to analyze the content
467        let (_blockquote_prefix, content) = Self::strip_blockquote_prefix(line);
468
469        let trimmed = content.trim_start();
470        let indent_len = content.len() - trimmed.len();
471
472        // Note: We intentionally do NOT apply heuristic detection to unordered list markers
473        // (*, -, +) because they have too many non-list uses: emphasis, globs, diffs, etc.
474        // The parser handles valid unordered list items; we only do heuristic detection
475        // for ordered lists where "1.Text" is almost always a list item with missing space.
476
477        // Check for ordered list markers (digits followed by .) without proper spacing
478        if let Some(dot_pos) = trimmed.find('.') {
479            let before_dot = &trimmed[..dot_pos];
480            if before_dot.chars().all(|c| c.is_ascii_digit()) && !before_dot.is_empty() {
481                let after_dot = &trimmed[dot_pos + 1..];
482                // Only flag if there's content directly after the marker (no space, no tab)
483                if !after_dot.is_empty() && !after_dot.starts_with(' ') && !after_dot.starts_with('\t') {
484                    let first_char = after_dot.chars().next().unwrap_or(' ');
485
486                    // For CLEAR user intent, only flag if:
487                    // 1. Starts with uppercase letter (strong list indicator), OR
488                    // 2. Starts with [ or ( (link/paren content)
489                    // Lowercase and digits are ambiguous (could be decimal, version, etc.)
490                    let is_clear_intent = first_char.is_ascii_uppercase() || first_char == '[' || first_char == '(';
491
492                    if is_clear_intent {
493                        let is_multi_line = self.is_multi_line_for_unrecognized(line_num, lines);
494                        let expected_spaces = self.get_expected_spaces(ListType::Ordered, is_multi_line);
495
496                        let marker = format!("{before_dot}.");
497                        let marker_pos = indent_len;
498                        let marker_end = marker_pos + marker.len();
499
500                        let (start_line, start_col, end_line, end_col) =
501                            calculate_match_range(line_num, line, marker_end, 0);
502
503                        let correct_spaces = " ".repeat(expected_spaces);
504                        let line_start_byte = ctx.line_offsets.get(line_num - 1).copied().unwrap_or(0);
505                        let fix_position = line_start_byte + marker_end;
506
507                        return Some(LintWarning {
508                            rule_name: Some("MD030".to_string()),
509                            severity: Severity::Warning,
510                            line: start_line,
511                            column: start_col,
512                            end_line,
513                            end_column: end_col,
514                            message: format!("Spaces after list markers (Expected: {expected_spaces}; Actual: 0)"),
515                            fix: Some(crate::rule::Fix {
516                                range: fix_position..fix_position,
517                                replacement: correct_spaces,
518                            }),
519                        });
520                    }
521                }
522            }
523        }
524
525        None
526    }
527
528    /// Simplified multi-line check for unrecognized list items
529    fn is_multi_line_for_unrecognized(&self, line_num: usize, lines: &[&str]) -> bool {
530        // For unrecognized list items, we can't rely on parser info
531        // Check if the next line exists and appears to be a continuation
532        if line_num < lines.len() {
533            let next_line = lines[line_num]; // line_num is 1-based, so this is the next line
534            let next_trimmed = next_line.trim();
535            // If next line is non-empty and indented, it might be a continuation
536            if !next_trimmed.is_empty() && next_line.starts_with(' ') {
537                return true;
538            }
539        }
540        false
541    }
542
543    /// Check if a line is part of an indented code block (4+ columns with blank line before)
544    fn is_indented_code_block(&self, line: &str, line_idx: usize, lines: &[&str]) -> bool {
545        // Must have 4+ columns of indentation (accounting for tab expansion)
546        if ElementCache::calculate_indentation_width_default(line) < 4 {
547            return false;
548        }
549
550        // If it's the first line, it's not an indented code block
551        if line_idx == 0 {
552            return false;
553        }
554
555        // Check if there's a blank line before this line or before the start of the indented block
556        if self.has_blank_line_before_indented_block(line_idx, lines) {
557            return true;
558        }
559
560        false
561    }
562
563    /// Check if there's a blank line before the start of an indented block
564    fn has_blank_line_before_indented_block(&self, line_idx: usize, lines: &[&str]) -> bool {
565        // Walk backwards to find the start of the indented block
566        let mut current_idx = line_idx;
567
568        // Find the first line in this indented block
569        while current_idx > 0 {
570            let current_line = lines[current_idx];
571            let prev_line = lines[current_idx - 1];
572
573            // If current line is not indented (< 4 columns), we've gone too far
574            if ElementCache::calculate_indentation_width_default(current_line) < 4 {
575                break;
576            }
577
578            // If previous line is not indented, check if it's blank
579            if ElementCache::calculate_indentation_width_default(prev_line) < 4 {
580                return prev_line.trim().is_empty();
581            }
582
583            current_idx -= 1;
584        }
585
586        false
587    }
588}
589
590#[cfg(test)]
591mod tests {
592    use super::*;
593    use crate::lint_context::LintContext;
594
595    #[test]
596    fn test_basic_functionality() {
597        let rule = MD030ListMarkerSpace::default();
598        let content = "* Item 1\n* Item 2\n  * Nested item\n1. Ordered item";
599        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
600        let result = rule.check(&ctx).unwrap();
601        assert!(
602            result.is_empty(),
603            "Correctly spaced list markers should not generate warnings"
604        );
605        let content = "*  Item 1 (too many spaces)\n* Item 2\n1.   Ordered item (too many spaces)";
606        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
607        let result = rule.check(&ctx).unwrap();
608        // Expect warnings for lines with too many spaces after the marker
609        assert_eq!(
610            result.len(),
611            2,
612            "Should flag lines with too many spaces after list marker"
613        );
614        for warning in result {
615            assert!(
616                warning.message.starts_with("Spaces after list markers (Expected:")
617                    && warning.message.contains("Actual:"),
618                "Warning message should include expected and actual values, got: '{}'",
619                warning.message
620            );
621        }
622    }
623
624    #[test]
625    fn test_nested_emphasis_not_flagged_issue_278() {
626        // Issue #278: Nested emphasis like *text **bold** more* should not trigger MD030
627        let rule = MD030ListMarkerSpace::default();
628
629        // This is emphasis with nested bold - NOT a list item
630        let content = "*This text is **very** important*";
631        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
632        let result = rule.check(&ctx).unwrap();
633        assert!(
634            result.is_empty(),
635            "Nested emphasis should not trigger MD030, got: {result:?}"
636        );
637
638        // Simple emphasis - NOT a list item
639        let content2 = "*Hello World*";
640        let ctx2 = LintContext::new(content2, crate::config::MarkdownFlavor::Standard, None);
641        let result2 = rule.check(&ctx2).unwrap();
642        assert!(
643            result2.is_empty(),
644            "Simple emphasis should not trigger MD030, got: {result2:?}"
645        );
646
647        // Bold text - NOT a list item
648        let content3 = "**bold text**";
649        let ctx3 = LintContext::new(content3, crate::config::MarkdownFlavor::Standard, None);
650        let result3 = rule.check(&ctx3).unwrap();
651        assert!(
652            result3.is_empty(),
653            "Bold text should not trigger MD030, got: {result3:?}"
654        );
655
656        // Bold+italic - NOT a list item
657        let content4 = "***bold and italic***";
658        let ctx4 = LintContext::new(content4, crate::config::MarkdownFlavor::Standard, None);
659        let result4 = rule.check(&ctx4).unwrap();
660        assert!(
661            result4.is_empty(),
662            "Bold+italic should not trigger MD030, got: {result4:?}"
663        );
664
665        // Actual list item with proper spacing - should NOT trigger
666        let content5 = "* Item with space";
667        let ctx5 = LintContext::new(content5, crate::config::MarkdownFlavor::Standard, None);
668        let result5 = rule.check(&ctx5).unwrap();
669        assert!(
670            result5.is_empty(),
671            "Properly spaced list item should not trigger MD030, got: {result5:?}"
672        );
673    }
674}