msg_tool\format/
fixed.rs

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    /// Whether to break words (ASCII only) at the end of the line.
98    break_words: bool,
99    /// Whether to insert a full-width space after a line break when a sentence starts with a full-width quotation mark.
100    insert_fullwidth_space_at_line_start: bool,
101    /// If a line break occurs in the middle of some symbols, bring the sentence to next line
102    break_with_sentence: bool,
103    #[cfg(feature = "jieba")]
104    /// Jieba instance for Chinese word segmentation.
105    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        // Store main content of the line (excluding commands and ruby)
243        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            // Check if we need to break and handle word breaking
276            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                    // Look back to find a good break point (space or non-ASCII)
370                    let mut break_pos = None;
371                    let mut temp_length = current_length;
372                    let mut j = result.len();
373
374                    // Find the last space or non-ASCII character position
375                    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 we found a good break point, move content after it to next line
390                    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    // Test break_words = false
685    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}