1use crate::decoder::TextChunk;
32use crate::{liaison_possible, syllabify_text, syllables};
33
34pub fn render_word_html(word: &str) -> String {
39 render_word_spans(&syllables(word))
40}
41
42pub fn render_html(text: &str) -> String {
52 let chunks = syllabify_text(text);
53 let mut out = String::with_capacity(text.len() * 4);
54 let mut previous_word_raw: Option<String> = None;
55
56 for chunk in &chunks {
57 match chunk {
58 TextChunk::Raw(s) => {
59 if !s.chars().all(char::is_whitespace) {
62 previous_word_raw = None;
63 }
64 out.push_str(&escape(s));
65 }
66 TextChunk::Word(sylls) => {
67 let word_raw: String = sylls.concat();
68 if let Some(prev) = &previous_word_raw {
69 if liaison_possible(prev, &word_raw) {
70 let consonant = liaison_consonant_for(prev);
71 out.push_str(&format!(
72 r#"<span class="liaison" data-with="{}"></span>"#,
73 consonant
74 ));
75 }
76 }
77 out.push_str(&render_word_spans(sylls));
78 previous_word_raw = Some(word_raw);
79 }
80 }
81 }
82
83 out
84}
85
86fn render_word_spans(sylls: &[String]) -> String {
87 if sylls.iter().all(|s| s.is_empty()) {
88 return String::new();
89 }
90 let mut s = String::from(r#"<span class="word">"#);
91 for (i, syl) in sylls.iter().enumerate() {
92 let class = if i % 2 == 0 { "syl syl-a" } else { "syl syl-b" };
93 s.push_str(&format!(
94 r#"<span class="{}">{}</span>"#,
95 class,
96 escape(syl)
97 ));
98 }
99 s.push_str("</span>");
100 s
101}
102
103fn liaison_consonant_for(prev: &str) -> &'static str {
108 let last = prev
109 .chars()
110 .rev()
111 .flat_map(|c| c.to_lowercase())
112 .next()
113 .unwrap_or(' ');
114 match last {
115 's' | 'x' | 'z' => "z",
116 'd' | 't' => "t",
117 'n' => "n",
118 'p' => "p",
119 'r' => "r",
120 'g' => "k",
121 _ => "z",
122 }
123}
124
125fn escape(s: &str) -> String {
126 let mut out = String::with_capacity(s.len());
127 for c in s.chars() {
128 match c {
129 '&' => out.push_str("&"),
130 '<' => out.push_str("<"),
131 '>' => out.push_str(">"),
132 '"' => out.push_str("""),
133 '\'' => out.push_str("'"),
134 _ => out.push(c),
135 }
136 }
137 out
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn mot_simple_3_syllabes() {
146 let html = render_word_html("chocolat");
147 assert_eq!(
148 html,
149 r#"<span class="word"><span class="syl syl-a">cho</span><span class="syl syl-b">co</span><span class="syl syl-a">lat</span></span>"#
150 );
151 }
152
153 #[test]
154 fn mot_alternance_demarre_a() {
155 assert!(render_word_html("famille")
156 .starts_with(r#"<span class="word"><span class="syl syl-a">fa</span>"#));
157 }
158
159 #[test]
160 fn texte_preserve_ponctuation() {
161 let html = render_html("le chat,");
162 assert!(html.contains(r#">le</span>"#));
163 assert!(html.contains(" "));
164 assert!(html.ends_with(","));
165 }
166
167 #[test]
168 fn liaison_les_hotels_emet_span() {
169 let html = render_html("les hôtels");
170 assert!(
171 html.contains(r#"<span class="liaison" data-with="z"></span>"#),
172 "liaison 'z' attendue, got: {}",
173 html
174 );
175 let pos_first_word = html.find("les").unwrap();
177 let pos_liaison = html.find(r#"class="liaison""#).unwrap();
178 let pos_second_word = html.find("ô").unwrap();
179 assert!(pos_first_word < pos_liaison);
180 assert!(pos_liaison < pos_second_word);
181 }
182
183 #[test]
184 fn liaison_absente_h_aspire() {
185 let html = render_html("les héros");
187 assert!(!html.contains(r#"class="liaison""#));
188 }
189
190 #[test]
191 fn liaison_absente_consonne_initiale() {
192 let html = render_html("les chats");
193 assert!(!html.contains(r#"class="liaison""#));
194 }
195
196 #[test]
197 fn liaison_consonne_t_pour_tout() {
198 let html = render_html("tout ami");
200 assert!(html.contains(r#"data-with="t""#), "got: {}", html);
201 }
202
203 #[test]
204 fn liaison_consonne_n_pour_en() {
205 let html = render_html("en automne");
207 assert!(html.contains(r#"data-with="n""#), "got: {}", html);
208 }
209
210 #[test]
211 fn liaison_bloquee_par_virgule() {
212 let html = render_html("les, hôtels");
214 assert!(!html.contains(r#"class="liaison""#), "got: {}", html);
215 }
216
217 #[test]
218 fn homographes_contexte_respecte() {
219 let html_nom = render_html("le couvent");
222 let html_verbe = render_html("elles couvent");
223 assert!(
225 html_nom.contains(r#">cou</span><span class="syl syl-b">vent</span>"#),
226 "got: {}",
227 html_nom
228 );
229 assert!(html_verbe.contains(">cou</span>"));
232 }
233
234 #[test]
235 fn html_echappe_caracteres_speciaux() {
236 let html = render_html("a < b");
238 assert!(html.contains("<"));
239 }
240
241 #[test]
242 fn texte_vide() {
243 assert_eq!(render_html(""), "");
244 }
245
246 #[test]
247 fn mot_vide_ne_crash_pas() {
248 assert_eq!(render_word_html(""), "");
250 }
251}