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(Finding::new(
40 SYNTAX_REPORTING_RULE_ID,
41 "syntax_reporting",
42 "major",
43 start_char,
44 end_char,
45 0.95,
46 "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.",
47 "linguistic",
48 norm[m.start()..m.end()].to_string(),
49 ));
50 }
51
52 findings
53}
54
55pub fn evaluate_syntax_parallel(text: &str) -> Vec<Finding> {
57 let norm = normalize_nfc(text);
58 let mapper = CharOffsetMap::new(&norm);
59 let mut findings = Vec::new();
60
61 for m in SYNTAX_PARALLEL_RE.find_iter(&norm) {
62 let (start_char, end_char) = mapper.convert_range(m.start(), m.end());
63 findings.push(Finding::new(
64 SYNTAX_PARALLEL_RULE_ID,
65 "syntax_parallel",
66 "major",
67 start_char,
68 end_char,
69 0.95,
70 "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.",
71 "linguistic",
72 norm[m.start()..m.end()].to_string(),
73 ));
74 }
75
76 findings
77}
78
79pub fn evaluate_syntax_cliche(text: &str) -> Vec<Finding> {
81 let norm = normalize_nfc(text);
82 let mapper = CharOffsetMap::new(&norm);
83 let mut findings = Vec::new();
84
85 for m in SYNTAX_CLICHE_RE.find_iter(&norm) {
86 let (start_char, end_char) = mapper.convert_range(m.start(), m.end());
87 findings.push(Finding::new(
88 SYNTAX_CLICHE_RULE_ID,
89 "syntax_cliche",
90 "minor",
91 start_char,
92 end_char,
93 0.95,
94 "Ghép nhầm hai kiểu câu: 'Như... đã biết' và '... đã biết rằng'.",
95 "linguistic",
96 norm[m.start()..m.end()].to_string(),
97 ));
98 }
99
100 findings
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn test_syntax_reporting() {
109 let text = "Phải nói là một quyết định sáng suốt.";
110 let findings = evaluate_syntax_reporting(text);
111 assert_eq!(findings.len(), 1);
112 assert_eq!(findings[0].pattern_id, SYNTAX_REPORTING_RULE_ID);
113 }
114
115 #[test]
116 fn test_syntax_parallel() {
117 let text = "Một kế hoạch hoàn hảo nhưng không khả thi.";
118 let findings = evaluate_syntax_parallel(text);
119 assert_eq!(findings.len(), 1);
120 assert_eq!(findings[0].pattern_id, SYNTAX_PARALLEL_RULE_ID);
121 }
122
123 #[test]
124 fn test_syntax_cliche() {
125 let text = "Như chúng ta đã biết rằng việc này rất khó.";
126 let findings = evaluate_syntax_cliche(text);
127 assert_eq!(findings.len(), 1);
128 assert_eq!(findings[0].pattern_id, SYNTAX_CLICHE_RULE_ID);
129 }
130}