1use crate::types::Finding;
2use crate::unicode_utils::{normalize_nfc, CharOffsetMap};
3use regex::RegexBuilder;
4use std::sync::LazyLock;
5
6static SYNTAX_REPORTING_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
7 RegexBuilder::new(r"^(phải nói là|cho là|cho rằng)\s+(?:một\s+)?(?:cú|sự|cuộc|việc|quyết định)\s+[^,.]+[.?!]?$")
8 .case_insensitive(true)
9 .build()
10 .expect("Regex CXH_REPORTING_VERB_INCOMPLETE_CLAUSE không hợp lệ")
11});
12
13static SYNTAX_PARALLEL_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
14 RegexBuilder::new(r"^(một\s+[^,]+?),?\s+nhưng\s+(không|chưa|thiếu)\b")
15 .case_insensitive(true)
16 .build()
17 .expect("Regex CXH_PARALLEL_CONJUNCTION_ASYMMETRY không hợp lệ")
18});
19
20static SYNTAX_CLICHE_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
21 RegexBuilder::new(r"\bnhư\s+[^,]+?\s+đã\s+biết\s+(?:là|rằng)\b")
22 .case_insensitive(true)
23 .build()
24 .expect("Regex CXH_HYBRID_COMPLEX_CONJUNCTION không hợp lệ")
25});
26
27pub const SYNTAX_REPORTING_RULE_ID: &str = "CXH_REPORTING_VERB_INCOMPLETE_CLAUSE";
28pub const SYNTAX_PARALLEL_RULE_ID: &str = "CXH_PARALLEL_CONJUNCTION_ASYMMETRY";
29pub const SYNTAX_CLICHE_RULE_ID: &str = "CXH_HYBRID_COMPLEX_CONJUNCTION";
30
31pub fn evaluate_syntax_reporting(text: &str) -> Vec<Finding> {
33 let norm = normalize_nfc(text);
34 let mapper = CharOffsetMap::new(&norm);
35 let mut findings = Vec::new();
36
37 for m in SYNTAX_REPORTING_RE.find_iter(&norm) {
38 let (start_char, end_char) = mapper.convert_range(m.start(), m.end());
39 findings.push(
40 Finding::new(
41 SYNTAX_REPORTING_RULE_ID,
42 "syntax_reporting",
43 "major",
44 start_char,
45 end_char,
46 0.95,
47 "Sau 'phải nói là' bắt buộc phải là một mệnh đề trọn vẹn, không dùng một danh ngữ đơn độc.",
48 "linguistic",
49 norm[m.start()..m.end()].to_string(),
50 )
51 .with_suggestion("Bổ sung vị ngữ để tạo mệnh đề hoàn chỉnh sau vị từ trần thuật"),
52 );
53 }
54
55 findings
56}
57
58pub fn evaluate_syntax_parallel(text: &str) -> Vec<Finding> {
60 let norm = normalize_nfc(text);
61 let mapper = CharOffsetMap::new(&norm);
62 let mut findings = Vec::new();
63
64 for m in SYNTAX_PARALLEL_RE.find_iter(&norm) {
65 let (start_char, end_char) = mapper.convert_range(m.start(), m.end());
66 findings.push(
67 Finding::new(
68 SYNTAX_PARALLEL_RULE_ID,
69 "syntax_parallel",
70 "major",
71 start_char,
72 end_char,
73 0.95,
74 "Liên từ 'nhưng' nối hai vế bất đối xứng: Danh ngữ đứng trước đối lập với cụm vị từ cụt phía sau.",
75 "linguistic",
76 norm[m.start()..m.end()].to_string(),
77 )
78 .with_suggestion("Cân bằng hai vế liên từ 'nhưng' cùng là danh ngữ hoặc cùng là vị ngữ trọn vẹn"),
79 );
80 }
81
82 findings
83}
84
85pub fn evaluate_syntax_cliche(text: &str) -> Vec<Finding> {
87 let norm = normalize_nfc(text);
88 let mapper = CharOffsetMap::new(&norm);
89 let mut findings = Vec::new();
90
91 for m in SYNTAX_CLICHE_RE.find_iter(&norm) {
92 let (start_char, end_char) = mapper.convert_range(m.start(), m.end());
93 findings.push(
94 Finding::new(
95 SYNTAX_CLICHE_RULE_ID,
96 "syntax_cliche",
97 "minor",
98 start_char,
99 end_char,
100 0.95,
101 "Ghép nhầm hai kiểu câu: 'Như... đã biết' và '... đã biết rằng'.",
102 "linguistic",
103 norm[m.start()..m.end()].to_string(),
104 )
105 .with_suggestion("Chỉ dùng 'Như... đã biết,' hoặc 'Chúng ta đều biết rằng...'"),
106 );
107 }
108
109 findings
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn test_syntax_reporting() {
118 let text = "Phải nói là một quyết định sáng suốt.";
119 let findings = evaluate_syntax_reporting(text);
120 assert_eq!(findings.len(), 1);
121 assert_eq!(findings[0].pattern_id, SYNTAX_REPORTING_RULE_ID);
122 }
123
124 #[test]
125 fn test_syntax_parallel() {
126 let text = "Một kế hoạch hoàn hảo nhưng không khả thi.";
127 let findings = evaluate_syntax_parallel(text);
128 assert_eq!(findings.len(), 1);
129 assert_eq!(findings[0].pattern_id, SYNTAX_PARALLEL_RULE_ID);
130 }
131
132 #[test]
133 fn test_syntax_cliche() {
134 let text = "Như chúng ta đã biết rằng việc này rất khó.";
135 let findings = evaluate_syntax_cliche(text);
136 assert_eq!(findings.len(), 1);
137 assert_eq!(findings[0].pattern_id, SYNTAX_CLICHE_RULE_ID);
138 }
139}