1use crate::types::*;
2use anyhow::Result;
3#[cfg(feature = "jieba")]
4use jieba_rs::Jieba;
5use unicode_segmentation::UnicodeSegmentation;
6
7const SPACE_STR_LIST: [&str; 2] = [" ", " "];
8const QUOTE_LIST: [(&str, &str); 4] = [("「", "」"), ("『", "』"), ("(", ")"), ("【", "】")];
9const BREAK_SENTENCE_SYMBOLS: [&str; 6] = ["…", ",", "。", "?", "!", "—"];
10
11fn check_is_ascii_alphanumeric(s: &str) -> bool {
12 for c in s.chars() {
13 if !c.is_ascii_alphanumeric() {
14 return false;
15 }
16 }
17 true
18}
19
20fn check_need_fullwidth_space(s: &str) -> bool {
21 let has_start_quote = QUOTE_LIST.iter().any(|(open, _)| s.starts_with(open));
22 if !has_start_quote {
23 return false;
24 }
25 for (open, close) in QUOTE_LIST.iter() {
26 let open_index = s.rfind(open);
27 if let Some(open_index) = open_index {
28 let index = s.rfind(close);
29 match index {
30 Some(idx) => {
31 return idx < open_index;
32 }
33 None => return true,
34 }
35 }
36 }
37 false
38}
39
40fn check_is_end_quote(segs: &[&str], pos: usize) -> bool {
41 let d = segs[pos];
42 QUOTE_LIST.iter().any(|(_, close)| d == *close)
43}
44
45fn check_is_end_quote_or_symbol(segs: &[&str], pos: usize) -> bool {
46 let d = segs[pos];
47 QUOTE_LIST.iter().any(|(_, close)| d == *close) || BREAK_SENTENCE_SYMBOLS.contains(&d)
48}
49
50fn check_is_start_quote(s: &str) -> bool {
51 QUOTE_LIST.iter().any(|(open, _)| s == *open)
52}
53
54fn take_trailing_start_quotes(buffer: &mut String) -> String {
55 let (collected, trailing) = {
56 let mut collected = buffer.graphemes(true).collect::<Vec<_>>();
57 let mut trailing = Vec::new();
58 while let Some(&last) = collected.last() {
59 if check_is_start_quote(last) {
60 collected.pop();
61 trailing.push(last);
62 } else {
63 break;
64 }
65 }
66 trailing.reverse();
67 (collected.concat(), trailing.concat())
68 };
69 *buffer = collected;
70 trailing
71}
72
73#[cfg(feature = "jieba")]
74fn check_chinese_word_is_break(segs: &[&str], pos: usize, jieba: &Jieba) -> bool {
75 let s = segs.join("");
76 let mut breaked = jieba
77 .cut(&s, false)
78 .iter()
79 .map(|s| s.graphemes(true).count())
80 .collect::<Vec<_>>();
81 let mut sum = 0;
82 for i in breaked.iter_mut() {
83 sum += *i;
84 *i = sum;
85 }
86 breaked.binary_search(&pos).is_err()
87}
88
89#[cfg(not(feature = "jieba"))]
90fn check_chinese_word_is_break(_segs: &[&str], _pos: usize, _jieba: &()) -> bool {
91 false
92}
93
94pub struct FixedFormatter {
95 length: usize,
96 keep_original: bool,
97 break_words: bool,
99 insert_fullwidth_space_at_line_start: bool,
101 break_with_sentence: bool,
103 #[cfg(feature = "jieba")]
104 jieba: Option<Jieba>,
106 #[cfg(not(feature = "jieba"))]
107 jieba: Option<()>,
108 #[allow(unused)]
109 typ: Option<ScriptType>,
110}
111
112impl FixedFormatter {
113 pub fn new(
114 length: usize,
115 keep_original: bool,
116 break_words: bool,
117 insert_fullwidth_space_at_line_start: bool,
118 break_with_sentence: bool,
119 #[cfg(feature = "jieba")] break_chinese_words: bool,
120 #[cfg(feature = "jieba")] jieba_dict: Option<String>,
121 typ: Option<ScriptType>,
122 ) -> Result<Self> {
123 #[cfg(feature = "jieba")]
124 let jieba = if !break_chinese_words {
125 let mut jieba = Jieba::new();
126 if let Some(dict) = jieba_dict {
127 let file = std::fs::File::open(dict)?;
128 let mut reader = std::io::BufReader::new(file);
129 jieba.load_dict(&mut reader)?;
130 }
131 Some(jieba)
132 } else {
133 None
134 };
135 Ok(FixedFormatter {
136 length,
137 keep_original,
138 break_words,
139 insert_fullwidth_space_at_line_start,
140 break_with_sentence,
141 #[cfg(feature = "jieba")]
142 jieba,
143 #[cfg(not(feature = "jieba"))]
144 jieba: None,
145 typ,
146 })
147 }
148
149 #[cfg(test)]
150 fn builder(length: usize) -> Self {
151 FixedFormatter {
152 length,
153 keep_original: false,
154 break_words: true,
155 insert_fullwidth_space_at_line_start: false,
156 break_with_sentence: false,
157 jieba: None,
158 typ: None,
159 }
160 }
161
162 #[cfg(test)]
163 fn keep_original(mut self, keep: bool) -> Self {
164 self.keep_original = keep;
165 self
166 }
167
168 #[cfg(test)]
169 fn break_words(mut self, break_words: bool) -> Self {
170 self.break_words = break_words;
171 self
172 }
173
174 #[cfg(test)]
175 fn insert_fullwidth_space_at_line_start(mut self, insert: bool) -> Self {
176 self.insert_fullwidth_space_at_line_start = insert;
177 self
178 }
179
180 #[cfg(test)]
181 fn break_with_sentence(mut self, break_with_sentence: bool) -> Self {
182 self.break_with_sentence = break_with_sentence;
183 self
184 }
185
186 #[cfg(all(feature = "jieba", test))]
187 fn break_chinese_words(mut self, break_chinese_words: bool) -> Result<Self> {
188 if !break_chinese_words {
189 let jieba = Jieba::new();
190 self.jieba = Some(jieba);
191 } else {
192 self.jieba = None;
193 }
194 Ok(self)
195 }
196
197 #[cfg(all(feature = "jieba", test))]
198 fn add_dict(mut self, dict: &str, freq: Option<usize>, tag: Option<&str>) -> Self {
199 if let Some(ref mut jieba) = self.jieba {
200 jieba.add_word(&dict, freq, tag);
201 }
202 self
203 }
204
205 #[cfg(test)]
206 #[allow(dead_code)]
207 fn typ(mut self, typ: Option<ScriptType>) -> Self {
208 self.typ = typ;
209 self
210 }
211
212 #[cfg(feature = "circus")]
213 fn is_circus(&self) -> bool {
214 matches!(self.typ, Some(ScriptType::Circus))
215 }
216
217 #[cfg(not(feature = "circus"))]
218 fn is_circus(&self) -> bool {
219 false
220 }
221
222 #[cfg(feature = "kirikiri")]
223 fn is_scn(&self) -> bool {
224 matches!(self.typ, Some(ScriptType::KirikiriScn))
225 }
226
227 #[cfg(not(feature = "kirikiri"))]
228 fn is_scn(&self) -> bool {
229 false
230 }
231
232 pub fn format(&self, message: &str) -> String {
233 let mut result = String::new();
234 let vec: Vec<_> = UnicodeSegmentation::graphemes(message, true).collect();
235 let mut current_length = 0;
236 let mut is_command = false;
237 let mut pre_is_lf = false;
238 let mut is_ruby = false;
239 let mut is_ruby_rt = false;
240 let mut last_command = None;
241 let mut i = 0;
242 let mut main_content = String::new();
244 let mut first_line = true;
245 let mut need_insert_fullwidth_space = false;
246
247 while i < vec.len() {
248 let grapheme = vec[i];
249
250 if grapheme == "\n" {
251 if self.keep_original
252 || (self.is_circus() && last_command.as_ref().is_some_and(|cmd| cmd == "@n"))
253 {
254 result.push('\n');
255 current_length = 0;
256 if first_line {
257 if self.insert_fullwidth_space_at_line_start {
258 if check_need_fullwidth_space(&main_content) {
259 need_insert_fullwidth_space = true;
260 }
261 }
262 }
263 if need_insert_fullwidth_space {
264 result.push(' ');
265 current_length += 1;
266 }
267 main_content.clear();
268 first_line = false;
269 }
270 pre_is_lf = true;
271 i += 1;
272 continue;
273 }
274
275 if current_length >= self.length {
277 if self.break_with_sentence
278 && !is_command
279 && !is_ruby_rt
280 && ((BREAK_SENTENCE_SYMBOLS.contains(&grapheme)
281 && i > 1
282 && BREAK_SENTENCE_SYMBOLS.contains(&vec[i - 1]))
283 || check_is_end_quote_or_symbol(&vec, i))
284 {
285 let mut break_pos = None;
286 let segs = result.graphemes(true).collect::<Vec<_>>();
287 let is_end_quote = check_is_end_quote(&vec, i);
288 let mut end = segs.len();
289 for (j, ch) in segs.iter().enumerate().rev() {
290 if BREAK_SENTENCE_SYMBOLS.contains(ch) {
291 end = j;
292 if !is_end_quote {
293 break_pos = Some(j);
294 }
295 }
296 break;
297 }
298 for (j, ch) in segs[..end].iter().enumerate().rev() {
299 if j >= end {
300 continue;
301 }
302 if BREAK_SENTENCE_SYMBOLS.contains(ch) {
303 break_pos = Some(j + 1);
304 break;
305 }
306 }
307 if let Some(pos) = break_pos {
308 let mut head = segs[..pos].concat();
309 let mut remaining = segs[pos..].concat();
310 if self.break_with_sentence {
311 let trailing = take_trailing_start_quotes(&mut head);
312 if !trailing.is_empty() {
313 remaining.insert_str(0, &trailing);
314 }
315 }
316 let remaining = remaining.trim_start().to_string();
317 result = head;
318 result.push('\n');
319 current_length = 0;
320 if first_line {
321 if self.insert_fullwidth_space_at_line_start {
322 if check_need_fullwidth_space(&main_content) {
323 need_insert_fullwidth_space = true;
324 }
325 }
326 first_line = false;
327 }
328 if need_insert_fullwidth_space {
329 result.push(' ');
330 current_length += 1;
331 }
332 result.push_str(&remaining);
333 current_length += remaining.graphemes(true).count();
334 main_content.clear();
335 pre_is_lf = true;
336 } else {
337 let trailing = if self.break_with_sentence {
338 take_trailing_start_quotes(&mut result)
339 } else {
340 String::new()
341 };
342 result.push('\n');
343 current_length = 0;
344 if first_line {
345 if self.insert_fullwidth_space_at_line_start {
346 if check_need_fullwidth_space(&main_content) {
347 need_insert_fullwidth_space = true;
348 }
349 }
350 first_line = false;
351 }
352 if need_insert_fullwidth_space {
353 result.push(' ');
354 current_length += 1;
355 }
356 main_content.clear();
357 if !trailing.is_empty() {
358 result.push_str(&trailing);
359 current_length += trailing.graphemes(true).count();
360 main_content.push_str(&trailing);
361 }
362 pre_is_lf = true;
363 }
364 } else if !self.break_words
365 && !is_command
366 && !is_ruby_rt
367 && check_is_ascii_alphanumeric(grapheme)
368 {
369 let mut break_pos = None;
371 let mut temp_length = current_length;
372 let mut j = result.len();
373
374 for ch in result.chars().rev() {
376 if ch == ' ' || ch == ' ' || !ch.is_ascii() {
377 break_pos = Some(j);
378 break;
379 }
380 if ch.is_ascii_alphabetic() {
381 temp_length -= 1;
382 if temp_length == 0 {
383 break;
384 }
385 }
386 j -= ch.len_utf8();
387 }
388
389 if let Some(pos) = break_pos {
391 let mut remaining = result[pos..].to_string();
392 result.truncate(pos);
393 if self.break_with_sentence {
394 let trailing = take_trailing_start_quotes(&mut result);
395 if !trailing.is_empty() {
396 remaining.insert_str(0, &trailing);
397 }
398 }
399 let remaining = remaining.trim_start().to_string();
400 result.push('\n');
401 current_length = 0;
402 if first_line {
403 if self.insert_fullwidth_space_at_line_start {
404 if check_need_fullwidth_space(&main_content) {
405 need_insert_fullwidth_space = true;
406 }
407 }
408 first_line = false;
409 }
410 if need_insert_fullwidth_space {
411 result.push(' ');
412 current_length += 1;
413 }
414 result.push_str(&remaining);
415 current_length += remaining.chars().count();
416 main_content.clear();
417 pre_is_lf = true;
418 } else {
419 let trailing = if self.break_with_sentence {
420 take_trailing_start_quotes(&mut result)
421 } else {
422 String::new()
423 };
424 result.push('\n');
425 current_length = 0;
426 if first_line {
427 if self.insert_fullwidth_space_at_line_start {
428 if check_need_fullwidth_space(&main_content) {
429 need_insert_fullwidth_space = true;
430 }
431 }
432 first_line = false;
433 }
434 if need_insert_fullwidth_space {
435 result.push(' ');
436 current_length += 1;
437 }
438 main_content.clear();
439 if !trailing.is_empty() {
440 result.push_str(&trailing);
441 current_length += trailing.graphemes(true).count();
442 main_content.push_str(&trailing);
443 }
444 pre_is_lf = true;
445 }
446 } else if self
447 .jieba
448 .as_ref()
449 .is_some_and(|s| check_chinese_word_is_break(&vec, i, s))
450 && !is_command
451 && !is_ruby_rt
452 {
453 #[cfg(feature = "jieba")]
454 {
455 let jieba = self.jieba.as_ref().unwrap();
456 let s = vec.join("");
457 let mut breaked = jieba
458 .cut(&s, false)
459 .iter()
460 .map(|s| s.graphemes(true).count())
461 .collect::<Vec<_>>();
462 let mut sum = 0;
463 for i in breaked.iter_mut() {
464 sum += *i;
465 *i = sum;
466 }
467 let break_pos = match breaked.binary_search(&i) {
468 Ok(pos) => Some(pos),
469 Err(pos) => {
470 if pos == 0 {
471 None
472 } else {
473 Some(pos - 1)
474 }
475 }
476 };
477 if let Some(break_pos) = break_pos {
478 let pos = breaked[break_pos];
479 let segs = result.graphemes(true).collect::<Vec<_>>();
480 let remain_count = i - pos;
481 let pos = segs.len() - remain_count;
482 let mut head = segs[..pos].concat();
483 let mut remaining = segs[pos..].concat();
484 if self.break_with_sentence {
485 let trailing = take_trailing_start_quotes(&mut head);
486 if !trailing.is_empty() {
487 remaining.insert_str(0, &trailing);
488 }
489 }
490 let remaining = remaining.trim_start().to_string();
491 result = head;
492 result.push('\n');
493 current_length = 0;
494 if first_line {
495 if self.insert_fullwidth_space_at_line_start {
496 if check_need_fullwidth_space(&main_content) {
497 need_insert_fullwidth_space = true;
498 }
499 }
500 first_line = false;
501 }
502 if need_insert_fullwidth_space {
503 result.push(' ');
504 current_length += 1;
505 }
506 result.push_str(&remaining);
507 current_length += remaining.graphemes(true).count();
508 main_content.clear();
509 pre_is_lf = true;
510 } else {
511 let trailing = if self.break_with_sentence {
512 take_trailing_start_quotes(&mut result)
513 } else {
514 String::new()
515 };
516 result.push('\n');
517 current_length = 0;
518 if first_line {
519 if self.insert_fullwidth_space_at_line_start {
520 if check_need_fullwidth_space(&main_content) {
521 need_insert_fullwidth_space = true;
522 }
523 }
524 first_line = false;
525 }
526 if need_insert_fullwidth_space {
527 result.push(' ');
528 current_length += 1;
529 }
530 main_content.clear();
531 if !trailing.is_empty() {
532 result.push_str(&trailing);
533 current_length += trailing.graphemes(true).count();
534 main_content.push_str(&trailing);
535 }
536 pre_is_lf = true;
537 }
538 }
539 } else {
540 let trailing = if self.break_with_sentence {
541 take_trailing_start_quotes(&mut result)
542 } else {
543 String::new()
544 };
545 result.push('\n');
546 current_length = 0;
547 if first_line {
548 if self.insert_fullwidth_space_at_line_start {
549 if check_need_fullwidth_space(&main_content) {
550 need_insert_fullwidth_space = true;
551 }
552 }
553 first_line = false;
554 }
555 if need_insert_fullwidth_space {
556 result.push(' ');
557 current_length += 1;
558 }
559 main_content.clear();
560 if !trailing.is_empty() {
561 result.push_str(&trailing);
562 current_length += trailing.graphemes(true).count();
563 main_content.push_str(&trailing);
564 }
565 pre_is_lf = true;
566 }
567 }
568
569 if (current_length == 0 || pre_is_lf) && SPACE_STR_LIST.contains(&grapheme) {
570 i += 1;
571 continue;
572 }
573
574 result.push_str(grapheme);
575
576 #[cfg(feature = "kirikiri")]
577 if self.is_scn() {
578 if grapheme == "#" {
579 i += 1;
580 while i < vec.len() && vec[i] != ";" {
581 result.push_str(vec[i]);
582 i += 1;
583 }
584 if i < vec.len() {
585 result.push_str(vec[i]);
586 i += 1;
587 }
588 continue;
589 }
590 if grapheme == "%" && i + 1 < vec.len() && vec[i + 1] == "r" {
591 result.push('r');
592 i += 2;
593 continue;
594 }
595 }
596
597 if self.is_circus() {
598 if grapheme == "@" {
599 is_command = true;
600 last_command = Some(String::new());
601 } else if is_command && grapheme.len() != 1
602 || !grapheme
603 .chars()
604 .next()
605 .unwrap_or(' ')
606 .is_ascii_alphanumeric()
607 {
608 is_command = false;
609 }
610 if grapheme == "{" {
611 is_ruby = true;
612 is_ruby_rt = true;
613 } else if is_ruby && grapheme == "/" {
614 is_ruby_rt = false;
615 i += 1;
616 continue;
617 } else if is_ruby && grapheme == "}" {
618 is_ruby = false;
619 i += 1;
620 continue;
621 }
622 }
623
624 if self.is_scn() {
625 if grapheme == "%" {
626 is_command = true;
627 } else if is_command && grapheme == ";" {
628 is_command = false;
629 i += 1;
630 continue;
631 }
632 if grapheme == "[" {
633 is_ruby = true;
634 is_ruby_rt = true;
635 i += 1;
636 continue;
637 } else if is_ruby && grapheme == "]" {
638 is_ruby = false;
639 is_ruby_rt = false;
640 i += 1;
641 continue;
642 }
643 }
644
645 if is_command {
646 if let Some(ref mut cmd) = last_command {
647 cmd.push_str(grapheme);
648 }
649 }
650
651 if !is_command && !is_ruby_rt {
652 current_length += 1;
653 main_content.push_str(grapheme);
654 }
655
656 pre_is_lf = false;
657 i += 1;
658 }
659
660 result
661 }
662}
663
664#[test]
665fn test_format() {
666 let formatter = FixedFormatter::builder(10);
667 let message = "This is a test message.\nThis is another line.";
668 let formatted_message = formatter.format(message);
669 assert_eq!(
670 formatted_message,
671 "This is a \ntest messa\nge.This is\nanother li\nne."
672 );
673 assert_eq!(formatter.format("● This is a test."), "● This is \na test.");
674 assert_eq!(
675 formatter.format("● This is a test."),
676 "● This is \na test."
677 );
678 let fommater2 = FixedFormatter::builder(10).keep_original(true);
679 assert_eq!(
680 fommater2.format("● Th\n is is a te st."),
681 "● Th\nis is a te\nst."
682 );
683
684 let no_break_formatter = FixedFormatter::builder(10).break_words(false);
686 assert_eq!(
687 no_break_formatter.format("Example text."),
688 "Example \ntext."
689 );
690
691 let no_break_formatter2 = FixedFormatter::builder(6).break_words(false);
692 assert_eq!(
693 no_break_formatter2.format("Example text."),
694 "Exampl\ne text\n."
695 );
696
697 let no_break_formatter3 = FixedFormatter::builder(7).break_words(false);
698 assert_eq!(
699 no_break_formatter3.format("Example text."),
700 "Example\ntext."
701 );
702
703 let real_world_no_break_formatter = FixedFormatter::builder(32).break_words(false);
704 assert_eq!(
705 real_world_no_break_formatter.format("○咕噜咕噜(Temporary Magnetic Pattern Linkage)"),
706 "○咕噜咕噜(Temporary Magnetic Pattern\nLinkage)"
707 );
708
709 let formatter3 = FixedFormatter::builder(10)
710 .break_words(false)
711 .insert_fullwidth_space_at_line_start(true);
712 assert_eq!(
713 formatter3.format("「This is a test."),
714 "「This is a\n\u{3000}test."
715 );
716
717 assert_eq!(
718 formatter3.format("(This) is a test."),
719 "(This) is \na test."
720 );
721
722 assert_eq!(
723 formatter3.format("(long text test here, test 1234"),
724 "(long text\n\u{3000}test here\n\u{3000}, test \n\u{3000}1234"
725 );
726
727 assert_eq!(
728 formatter3.format("(This) 「is a test."),
729 "(This) 「is\n\u{3000}a test."
730 );
731
732 let formatter4 = FixedFormatter::builder(10)
733 .break_words(false)
734 .break_with_sentence(true);
735 assert_eq!(
736 formatter4.format("『打断测,测试一下……』"),
737 "『打断测,\n测试一下……』"
738 );
739
740 assert_eq!(
741 formatter4.format("『打断测,测试一下。』"),
742 "『打断测,\n测试一下。』"
743 );
744
745 assert_eq!(
746 formatter4.format("『打断是测试一下哦……』"),
747 "『打断是测试一下哦\n……』"
748 );
749
750 assert_eq!(
751 formatter4.format("『打断测是测试一下。』"),
752 "『打断测是测试一下。\n』"
753 );
754
755 assert_eq!(
756 formatter4.format("『打断测试,测试一下。』"),
757 "『打断测试,\n测试一下。』"
758 );
759
760 assert_eq!(
761 formatter4.format("这打断测试,测试一下。"),
762 "这打断测试,\n测试一下。"
763 );
764
765 assert_eq!(
766 formatter4.format("这打断测试哦测试一下。。"),
767 "这打断测试哦测试一下\n。。"
768 );
769
770 let formatter5 = FixedFormatter::builder(10)
771 .break_words(false)
772 .insert_fullwidth_space_at_line_start(true)
773 .break_with_sentence(true);
774 assert_eq!(
775 formatter5.format("「一二三四『whatthe』"),
776 "「一二三四\n\u{3000}『whatthe』"
777 );
778
779 let real_break_formatter = FixedFormatter::builder(27)
780 .break_words(false)
781 .break_with_sentence(true);
782 assert_eq!(
783 real_break_formatter.format("「他们就是想和阳见待在一个社团,在里面表现表现、耍耍帅,这样不就和她套上近乎了嘛!算盘珠子都打到我脸上了……」"),
784 "「他们就是想和阳见待在一个社团,\n在里面表现表现、耍耍帅,这样不就和她套上近乎了嘛!算盘\n珠子都打到我脸上了……」"
785 );
786
787 assert_eq!(
788 real_break_formatter
789 .format("「在英山的话或许可以看看『moon river』『Lavir』或是『Patisserie Yuzuru』」"),
790 "「在英山的话或许可以看看『moon river』\n『Lavir』或是『Patisserie Yuzuru\n』」"
791 );
792
793 #[cfg(feature = "circus")]
794 {
795 let circus_formatter = FixedFormatter::builder(10).typ(Some(ScriptType::Circus));
796 assert_eq!(
797 circus_formatter.format("● @cmd1@cmd2@cmd3中文字数是一\n 二三 四五六七八九十"),
798 "● @cmd1@cmd2@cmd3中文字数是一二三\n四五六七八九十"
799 );
800 assert_eq!(
801 circus_formatter
802 .format("● @cmd1@cmd2@cmd3{rubyText/中文}字数是一\n 二三 四五六七八九十"),
803 "● @cmd1@cmd2@cmd3{rubyText/中文}字数是一二三\n四五六七八九十"
804 );
805 let circus_formatter2 = FixedFormatter::builder(32).typ(Some(ScriptType::Circus));
806 assert_eq!(
807 circus_formatter2.format("@re1@re2@b1@t30@w1「当然现在我很幸福哦?\n 因为有你在身边」@n\n「@b1@t38@w1当然现在我很幸福哦?\n 因为有敦也君在身边」"),
808 "@re1@re2@b1@t30@w1「当然现在我很幸福哦?因为有你在身边」@n\n「@b1@t38@w1当然现在我很幸福哦?因为有敦也君在身边」"
809 );
810 }
811
812 #[cfg(feature = "kirikiri")]
813 {
814 let scn_formatter = FixedFormatter::builder(3)
815 .break_words(false)
816 .typ(Some(ScriptType::KirikiriScn));
817 assert_eq!(
818 scn_formatter.format("%test;[ruby]测[test]试打断。"),
819 "%test;[ruby]测[test]试打\n断。"
820 );
821 assert_eq!(
822 scn_formatter.format("%f$ハート$;#00ffadd6;♥%r打断测试"),
823 "%f$ハート$;#00ffadd6;♥%r打断\n测试"
824 )
825 }
826 #[cfg(feature = "jieba")]
827 {
828 let jieba_formatter = FixedFormatter::builder(8)
829 .break_words(false)
830 .break_chinese_words(false)
831 .unwrap();
832 assert_eq!(
833 jieba_formatter.format("测试分词,我们中出了一个叛徒。"),
834 "测试分词,我们中\n出了一个叛徒。"
835 );
836 let jieba_formatter2 = FixedFormatter::builder(8)
837 .break_words(false)
838 .break_chinese_words(false)
839 .unwrap()
840 .add_dict("中出", Some(114514), None);
841 assert_eq!(
842 jieba_formatter2
843 .jieba
844 .as_ref()
845 .is_some_and(|s| s.has_word("中出")),
846 true
847 );
848 assert_eq!(
849 jieba_formatter2.format("测试分词,我们中出了一个叛徒。"),
850 "测试分词,我们\n中出了一个叛徒。"
851 );
852 }
853}