1use std::borrow::Cow;
6use std::char::{ToLowercase, ToUppercase};
7use std::ops::Range;
8
9use icu_segmenter::WordSegmenter;
10use layout_api::{LayoutNode, SharedSelection};
11use style::computed_values::_webkit_text_security::T as WebKitTextSecurity;
12use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
13use style::selector_parser::PseudoElement;
14use style::values::specified::text::TextTransformCase;
15use unicode_bidi::Level;
16use unicode_categories::UnicodeCategories;
17
18use super::text_run::TextRun;
19use super::{
20 InlineBox, InlineBoxIdentifier, InlineBoxes, InlineFormattingContext, InlineItem,
21 SharedInlineStyles,
22};
23use crate::cell::ArcRefCell;
24use crate::context::LayoutContext;
25use crate::dom::{LayoutBox, NodeExt};
26use crate::dom_traversal::NodeAndStyleInfo;
27use crate::flow::BlockLevelBox;
28use crate::flow::float::FloatBox;
29use crate::formatting_contexts::IndependentFormattingContext;
30use crate::positioned::AbsolutelyPositionedBox;
31use crate::style_ext::ComputedValuesExt;
32
33#[derive(Default)]
34pub(crate) struct InlineFormattingContextBuilder {
35 pub shared_inline_styles_stack: Vec<SharedInlineStyles>,
40
41 pub text_segments: Vec<String>,
44
45 current_text_offset: usize,
48
49 current_character_offset: usize,
53
54 pub shared_selection: Option<SharedSelection>,
57
58 last_inline_box_ended_with_collapsible_white_space: bool,
66
67 on_word_boundary: bool,
70
71 pub contains_floats: bool,
73
74 pub inline_items: Vec<InlineItem>,
78
79 pub inline_boxes: InlineBoxes,
81
82 inline_box_stack: Vec<InlineBoxIdentifier>,
91
92 pub is_empty: bool,
96
97 has_processed_first_letter: bool,
100}
101
102impl InlineFormattingContextBuilder {
103 pub(crate) fn new(info: &NodeAndStyleInfo, context: &LayoutContext) -> Self {
104 Self {
105 on_word_boundary: true,
107 is_empty: true,
108 shared_inline_styles_stack: vec![SharedInlineStyles::from_info_and_context(
109 info, context,
110 )],
111 shared_selection: info.node.selection(),
112 ..Default::default()
113 }
114 }
115
116 pub(crate) fn currently_processing_inline_box(&self) -> bool {
117 !self.inline_box_stack.is_empty()
118 }
119
120 fn push_control_character_string(&mut self, string_to_push: &str) {
121 self.text_segments.push(string_to_push.to_owned());
122 self.current_text_offset += string_to_push.len();
123 self.current_character_offset += string_to_push.chars().count();
124 }
125
126 fn shared_inline_styles(&self) -> SharedInlineStyles {
127 self.shared_inline_styles_stack
128 .last()
129 .expect("Should always have at least one SharedInlineStyles")
130 .clone()
131 }
132
133 pub(crate) fn push_atomic(
134 &mut self,
135 independent_formatting_context_creator: impl FnOnce()
136 -> ArcRefCell<IndependentFormattingContext>,
137 old_layout_box: Option<LayoutBox>,
138 ) -> InlineItem {
139 let independent_formatting_context = old_layout_box
141 .and_then(|layout_box| match layout_box {
142 LayoutBox::InlineLevel(InlineItem::Atomic(atomic, ..)) => Some(atomic),
143 _ => None,
144 })
145 .unwrap_or_else(independent_formatting_context_creator);
146
147 let inline_level_box = InlineItem::Atomic(
148 independent_formatting_context,
149 self.current_text_offset,
150 Level::ltr(), );
152 self.inline_items.push(inline_level_box.clone());
153 self.is_empty = false;
154
155 self.push_control_character_string("\u{fffc}");
158
159 self.last_inline_box_ended_with_collapsible_white_space = false;
160 self.on_word_boundary = true;
161
162 self.has_processed_first_letter = true;
164
165 inline_level_box
166 }
167
168 pub(crate) fn push_absolutely_positioned_box(
169 &mut self,
170 absolutely_positioned_box_creator: impl FnOnce() -> ArcRefCell<AbsolutelyPositionedBox>,
171 old_layout_box: Option<LayoutBox>,
172 ) -> InlineItem {
173 let absolutely_positioned_box = old_layout_box
174 .and_then(|layout_box| match layout_box {
175 LayoutBox::InlineLevel(InlineItem::OutOfFlowAbsolutelyPositionedBox(
176 positioned_box,
177 ..,
178 )) => Some(positioned_box),
179 _ => None,
180 })
181 .unwrap_or_else(absolutely_positioned_box_creator);
182
183 let inline_level_box = InlineItem::OutOfFlowAbsolutelyPositionedBox(
185 absolutely_positioned_box,
186 self.current_text_offset,
187 );
188
189 self.inline_items.push(inline_level_box.clone());
190 self.is_empty = false;
191 inline_level_box
192 }
193
194 pub(crate) fn push_float_box(
195 &mut self,
196 float_box_creator: impl FnOnce() -> ArcRefCell<FloatBox>,
197 old_layout_box: Option<LayoutBox>,
198 ) -> InlineItem {
199 let inline_level_box = old_layout_box
200 .and_then(|layout_box| match layout_box {
201 LayoutBox::InlineLevel(inline_item) => Some(inline_item),
202 _ => None,
203 })
204 .unwrap_or_else(|| InlineItem::OutOfFlowFloatBox(float_box_creator()));
205
206 debug_assert!(
207 matches!(inline_level_box, InlineItem::OutOfFlowFloatBox(..),),
208 "Created float box with incompatible `old_layout_box`"
209 );
210
211 self.inline_items.push(inline_level_box.clone());
212 self.is_empty = false;
213 self.contains_floats = true;
214 inline_level_box
215 }
216
217 pub(crate) fn push_block_level_box(&mut self, block_level: ArcRefCell<BlockLevelBox>) {
218 assert!(self.currently_processing_inline_box());
219 self.contains_floats = self.contains_floats || block_level.borrow().contains_floats();
220 self.inline_items.push(InlineItem::BlockLevel(block_level));
221 }
222
223 pub(crate) fn start_inline_box(
224 &mut self,
225 inline_box_creator: impl FnOnce() -> ArcRefCell<InlineBox>,
226 old_layout_box: Option<LayoutBox>,
227 ) -> InlineItem {
228 let inline_box = old_layout_box
230 .and_then(|layout_box| match layout_box {
231 LayoutBox::InlineLevel(InlineItem::StartInlineBox(inline_box)) => Some(inline_box),
232 _ => None,
233 })
234 .unwrap_or_else(inline_box_creator);
235
236 let borrowed_inline_box = inline_box.borrow();
237 self.push_control_character_string(borrowed_inline_box.base.style.bidi_control_chars().0);
238
239 self.shared_inline_styles_stack
240 .push(borrowed_inline_box.shared_inline_styles.clone());
241 std::mem::drop(borrowed_inline_box);
242
243 let identifier = self.inline_boxes.start_inline_box(inline_box.clone());
244 let inline_item = InlineItem::StartInlineBox(inline_box);
245 self.inline_items.push(inline_item.clone());
246 self.inline_box_stack.push(identifier);
247 self.is_empty = false;
248 inline_item
249 }
250
251 pub(crate) fn end_inline_box(&mut self) {
256 self.shared_inline_styles_stack.pop();
257 self.inline_items.push(InlineItem::EndInlineBox);
258 let identifier = self
259 .inline_box_stack
260 .pop()
261 .expect("Ended non-existent inline box");
262 self.inline_boxes.end_inline_box(identifier);
263 let inline_level_box = self.inline_boxes.get(&identifier);
264 let bidi_control_chars = inline_level_box.borrow().base.style.bidi_control_chars();
265 self.push_control_character_string(bidi_control_chars.1);
266 }
267
268 pub(crate) fn push_text_with_possible_first_letter<'dom>(
276 &mut self,
277 text: Cow<'dom, str>,
278 info: &NodeAndStyleInfo<'dom>,
279 container_info: &NodeAndStyleInfo<'dom>,
280 layout_context: &LayoutContext,
281 ) -> bool {
282 if self.has_processed_first_letter || !container_info.pseudo_element_chain().is_empty() {
283 self.push_text(text, info);
284 return false;
285 }
286
287 let Some(first_letter_info) =
288 container_info.with_pseudo_element(layout_context, PseudoElement::FirstLetter)
289 else {
290 self.push_text(text, info);
291 return false;
292 };
293
294 let first_letter_range = first_letter_range(&text[..]);
295 if first_letter_range.is_empty() {
296 return false;
297 }
298
299 if first_letter_range.start != 0 {
301 self.push_text(Cow::Borrowed(&text[0..first_letter_range.start]), info);
302 }
303
304 let box_slot = first_letter_info.node.box_slot();
306 let inline_item = self.start_inline_box(
307 || ArcRefCell::new(InlineBox::new(&first_letter_info, layout_context)),
308 None,
309 );
310 box_slot.set(LayoutBox::InlineLevel(inline_item));
311
312 let first_letter_text = Cow::Borrowed(&text[first_letter_range.clone()]);
313 self.push_text(first_letter_text, &first_letter_info);
314 self.end_inline_box();
315 self.has_processed_first_letter = true;
316
317 self.push_text(Cow::Borrowed(&text[first_letter_range.end..]), info);
319
320 true
321 }
322
323 pub(crate) fn push_text<'dom>(&mut self, text: Cow<'dom, str>, info: &NodeAndStyleInfo<'dom>) {
324 let white_space_collapse = info.style.clone_white_space_collapse();
325 let collapsed = WhitespaceCollapse::new(
326 text.chars(),
327 white_space_collapse,
328 self.last_inline_box_ended_with_collapsible_white_space,
329 );
330
331 let text_transform = info.style.clone_text_transform().case();
334 let capitalized_text: String;
335 let char_iterator: Box<dyn Iterator<Item = char>> = match text_transform {
336 TextTransformCase::None => Box::new(collapsed),
337 TextTransformCase::Capitalize => {
338 let collapsed_string: String = collapsed.collect();
345 capitalized_text = capitalize_string(&collapsed_string, self.on_word_boundary);
346 Box::new(capitalized_text.chars())
347 },
348 _ => {
349 Box::new(TextTransformation::new(collapsed, text_transform))
352 },
353 };
354
355 let char_iterator = if info.style.clone__webkit_text_security() != WebKitTextSecurity::None
356 {
357 Box::new(TextSecurityTransform::new(
358 char_iterator,
359 info.style.clone__webkit_text_security(),
360 ))
361 } else {
362 char_iterator
363 };
364
365 let white_space_collapse = info.style.clone_white_space_collapse();
366 let mut character_count = 0;
367 let new_text: String = char_iterator
368 .inspect(|&character| {
369 character_count += 1;
370
371 self.is_empty = self.is_empty &&
372 match white_space_collapse {
373 WhiteSpaceCollapse::Collapse => character.is_ascii_whitespace(),
374 WhiteSpaceCollapse::PreserveBreaks => {
375 character.is_ascii_whitespace() && character != '\n'
376 },
377 WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces => false,
378 };
379 })
380 .collect();
381
382 if new_text.is_empty() {
383 return;
384 }
385
386 if let Some(last_character) = new_text.chars().next_back() {
387 self.on_word_boundary = last_character.is_whitespace();
388 self.last_inline_box_ended_with_collapsible_white_space =
389 self.on_word_boundary && white_space_collapse != WhiteSpaceCollapse::Preserve;
390 }
391
392 let new_range = self.current_text_offset..self.current_text_offset + new_text.len();
393 self.current_text_offset = new_range.end;
394
395 let new_character_range =
396 self.current_character_offset..self.current_character_offset + character_count;
397 self.current_character_offset = new_character_range.end;
398
399 self.text_segments.push(new_text);
400
401 let current_inline_styles = self.shared_inline_styles();
402
403 if let Some(InlineItem::TextRun(text_run)) = self.inline_items.last() {
404 if text_run
405 .borrow()
406 .inline_styles
407 .ptr_eq(¤t_inline_styles)
408 {
409 text_run.borrow_mut().text_range.end = new_range.end;
410 text_run.borrow_mut().character_range.end = new_character_range.end;
411 return;
412 }
413 }
414
415 self.inline_items
416 .push(InlineItem::TextRun(ArcRefCell::new(TextRun::new(
417 info.into(),
418 current_inline_styles,
419 new_range,
420 new_character_range,
421 ))));
422 }
423
424 pub(crate) fn enter_display_contents(&mut self, shared_inline_styles: SharedInlineStyles) {
425 self.shared_inline_styles_stack.push(shared_inline_styles);
426 }
427
428 pub(crate) fn leave_display_contents(&mut self) {
429 self.shared_inline_styles_stack.pop();
430 }
431
432 pub(crate) fn finish(
434 self,
435 layout_context: &LayoutContext,
436 has_first_formatted_line: bool,
437 is_single_line_text_input: bool,
438 default_bidi_level: Level,
439 ) -> Option<InlineFormattingContext> {
440 if self.is_empty {
441 return None;
442 }
443
444 assert!(self.inline_box_stack.is_empty());
445 Some(InlineFormattingContext::new_with_builder(
446 self,
447 layout_context,
448 has_first_formatted_line,
449 is_single_line_text_input,
450 default_bidi_level,
451 ))
452 }
453}
454
455fn preserve_segment_break() -> bool {
456 true
457}
458
459pub struct WhitespaceCollapse<InputIterator> {
460 char_iterator: InputIterator,
461 white_space_collapse: WhiteSpaceCollapse,
462
463 remove_collapsible_white_space_at_start: bool,
467
468 following_newline: bool,
471
472 have_seen_non_white_space_characters: bool,
475
476 inside_white_space: bool,
480
481 character_pending_to_return: Option<char>,
485}
486
487impl<InputIterator> WhitespaceCollapse<InputIterator> {
488 pub fn new(
489 char_iterator: InputIterator,
490 white_space_collapse: WhiteSpaceCollapse,
491 trim_beginning_white_space: bool,
492 ) -> Self {
493 Self {
494 char_iterator,
495 white_space_collapse,
496 remove_collapsible_white_space_at_start: trim_beginning_white_space,
497 inside_white_space: false,
498 following_newline: false,
499 have_seen_non_white_space_characters: false,
500 character_pending_to_return: None,
501 }
502 }
503
504 fn is_leading_trimmed_white_space(&self) -> bool {
505 !self.have_seen_non_white_space_characters && self.remove_collapsible_white_space_at_start
506 }
507
508 fn need_to_produce_space_character_after_white_space(&self) -> bool {
513 self.inside_white_space && !self.following_newline && !self.is_leading_trimmed_white_space()
514 }
515}
516
517impl<InputIterator> Iterator for WhitespaceCollapse<InputIterator>
518where
519 InputIterator: Iterator<Item = char>,
520{
521 type Item = char;
522
523 fn next(&mut self) -> Option<Self::Item> {
524 if self.white_space_collapse == WhiteSpaceCollapse::Preserve ||
530 self.white_space_collapse == WhiteSpaceCollapse::BreakSpaces
531 {
532 return match self.char_iterator.next() {
537 Some('\r') => Some(' '),
538 next => next,
539 };
540 }
541
542 if let Some(character) = self.character_pending_to_return.take() {
543 self.inside_white_space = false;
544 self.have_seen_non_white_space_characters = true;
545 self.following_newline = false;
546 return Some(character);
547 }
548
549 while let Some(character) = self.char_iterator.next() {
550 if character.is_ascii_whitespace() && character != '\n' {
554 self.inside_white_space = true;
555 continue;
556 }
557
558 if character == '\n' {
562 if self.white_space_collapse != WhiteSpaceCollapse::Collapse {
568 self.inside_white_space = false;
569 self.following_newline = true;
570 return Some(character);
571
572 } else if !self.following_newline &&
578 preserve_segment_break() &&
579 !self.is_leading_trimmed_white_space()
580 {
581 self.inside_white_space = false;
582 self.following_newline = true;
583 return Some(' ');
584 } else {
585 self.following_newline = true;
586 continue;
587 }
588 }
589
590 if self.need_to_produce_space_character_after_white_space() {
599 self.inside_white_space = false;
600 self.character_pending_to_return = Some(character);
601 return Some(' ');
602 }
603
604 self.inside_white_space = false;
605 self.have_seen_non_white_space_characters = true;
606 self.following_newline = false;
607 return Some(character);
608 }
609
610 if self.need_to_produce_space_character_after_white_space() {
611 self.inside_white_space = false;
612 return Some(' ');
613 }
614
615 None
616 }
617
618 fn size_hint(&self) -> (usize, Option<usize>) {
619 self.char_iterator.size_hint()
620 }
621
622 fn count(self) -> usize
623 where
624 Self: Sized,
625 {
626 self.char_iterator.count()
627 }
628}
629
630enum PendingCaseConversionResult {
631 Uppercase(ToUppercase),
632 Lowercase(ToLowercase),
633}
634
635impl PendingCaseConversionResult {
636 fn next(&mut self) -> Option<char> {
637 match self {
638 PendingCaseConversionResult::Uppercase(to_uppercase) => to_uppercase.next(),
639 PendingCaseConversionResult::Lowercase(to_lowercase) => to_lowercase.next(),
640 }
641 }
642}
643
644pub struct TextTransformation<InputIterator> {
649 char_iterator: InputIterator,
651 text_transform: TextTransformCase,
653 pending_case_conversion_result: Option<PendingCaseConversionResult>,
656}
657
658impl<InputIterator> TextTransformation<InputIterator> {
659 pub fn new(char_iterator: InputIterator, text_transform: TextTransformCase) -> Self {
660 Self {
661 char_iterator,
662 text_transform,
663 pending_case_conversion_result: None,
664 }
665 }
666}
667
668impl<InputIterator> Iterator for TextTransformation<InputIterator>
669where
670 InputIterator: Iterator<Item = char>,
671{
672 type Item = char;
673
674 fn next(&mut self) -> Option<Self::Item> {
675 if let Some(character) = self
676 .pending_case_conversion_result
677 .as_mut()
678 .and_then(|result| result.next())
679 {
680 return Some(character);
681 }
682 self.pending_case_conversion_result = None;
683
684 for character in self.char_iterator.by_ref() {
685 match self.text_transform {
686 TextTransformCase::None => return Some(character),
687 TextTransformCase::Uppercase => {
688 let mut pending_result =
689 PendingCaseConversionResult::Uppercase(character.to_uppercase());
690 if let Some(character) = pending_result.next() {
691 self.pending_case_conversion_result = Some(pending_result);
692 return Some(character);
693 }
694 },
695 TextTransformCase::Lowercase => {
696 let mut pending_result =
697 PendingCaseConversionResult::Lowercase(character.to_lowercase());
698 if let Some(character) = pending_result.next() {
699 self.pending_case_conversion_result = Some(pending_result);
700 return Some(character);
701 }
702 },
703 TextTransformCase::Capitalize => return Some(character),
706 }
707 }
708 None
709 }
710}
711
712pub struct TextSecurityTransform<InputIterator> {
713 char_iterator: InputIterator,
715 text_security: WebKitTextSecurity,
717}
718
719impl<InputIterator> TextSecurityTransform<InputIterator> {
720 pub fn new(char_iterator: InputIterator, text_security: WebKitTextSecurity) -> Self {
721 Self {
722 char_iterator,
723 text_security,
724 }
725 }
726}
727
728impl<InputIterator> Iterator for TextSecurityTransform<InputIterator>
729where
730 InputIterator: Iterator<Item = char>,
731{
732 type Item = char;
733
734 fn next(&mut self) -> Option<Self::Item> {
735 Some(match self.char_iterator.next()? {
739 '\u{200B}' => '\u{200B}',
743 '\n' => '\n',
745 character => match self.text_security {
746 WebKitTextSecurity::None => character,
747 WebKitTextSecurity::Circle => '○',
748 WebKitTextSecurity::Disc => '●',
749 WebKitTextSecurity::Square => '■',
750 },
751 })
752 }
753}
754
755pub(crate) fn capitalize_string(string: &str, allow_word_at_start: bool) -> String {
758 let mut output_string = String::new();
759 output_string.reserve(string.len());
760
761 let word_segmenter = WordSegmenter::new_auto();
762 let mut bounds = word_segmenter.segment_str(string).peekable();
763 let mut byte_index = 0;
764 for character in string.chars() {
765 let current_byte_index = byte_index;
766 byte_index += character.len_utf8();
767
768 if let Some(next_index) = bounds.peek() {
769 if *next_index == current_byte_index {
770 bounds.next();
771
772 if current_byte_index != 0 || allow_word_at_start {
773 output_string.extend(character.to_uppercase());
774 continue;
775 }
776 }
777 }
778
779 output_string.push(character);
780 }
781
782 output_string
783}
784
785fn first_letter_range(text: &str) -> Range<usize> {
795 enum State {
796 Start,
798 PrecedingPunctuation,
800 Lns,
802 TrailingPunctuation,
805 }
806
807 let mut start = 0;
808 let mut state = State::Start;
809 for (index, character) in text.char_indices() {
810 match &mut state {
811 State::Start => {
812 if character.is_letter() || character.is_number() || character.is_symbol() {
813 start = index;
814 state = State::Lns;
815 } else if character.is_punctuation() {
816 start = index;
817 state = State::PrecedingPunctuation
818 }
819 },
820 State::PrecedingPunctuation => {
821 if character.is_letter() || character.is_number() || character.is_symbol() {
822 state = State::Lns;
823 } else if !character.is_separator_space() && !character.is_punctuation() {
824 return 0..0;
825 }
826 },
827 State::Lns => {
828 if character.is_punctuation() &&
831 !character.is_punctuation_open() &&
832 !character.is_punctuation_dash()
833 {
834 state = State::TrailingPunctuation;
835 } else {
836 return start..index;
837 }
838 },
839 State::TrailingPunctuation => {
840 if character.is_punctuation() &&
843 !character.is_punctuation_open() &&
844 !character.is_punctuation_dash()
845 {
846 continue;
847 } else {
848 return start..index;
849 }
850 },
851 }
852 }
853
854 match state {
855 State::Start | State::PrecedingPunctuation => 0..0,
856 State::Lns | State::TrailingPunctuation => start..text.len(),
857 }
858}
859
860#[cfg(test)]
861mod tests {
862 use super::*;
863
864 fn assert_first_letter_eq(text: &str, expected: &str) {
865 let range = first_letter_range(text);
866 assert_eq!(&text[range], expected);
867 }
868
869 #[test]
870 fn test_first_letter_range() {
871 assert_first_letter_eq("", "");
873 assert_first_letter_eq(" ", "");
874
875 assert_first_letter_eq("(", "");
877 assert_first_letter_eq(" (", "");
878 assert_first_letter_eq("( ", "");
879 assert_first_letter_eq("()", "");
880
881 assert_first_letter_eq("\u{0903}", "");
883
884 assert_first_letter_eq("A", "A");
886 assert_first_letter_eq(" A", "A");
887 assert_first_letter_eq("A ", "A");
888 assert_first_letter_eq(" A ", "A");
889
890 assert_first_letter_eq("App", "A");
892 assert_first_letter_eq(" App", "A");
893 assert_first_letter_eq("App ", "A");
894
895 assert_first_letter_eq(r#""A"#, r#""A"#);
897 assert_first_letter_eq(r#" "A"#, r#""A"#);
898 assert_first_letter_eq(r#""A "#, r#""A"#);
899 assert_first_letter_eq(r#"" A"#, r#"" A"#);
900 assert_first_letter_eq(r#" "A "#, r#""A"#);
901 assert_first_letter_eq(r#"("A"#, r#"("A"#);
902 assert_first_letter_eq(r#" ("A"#, r#"("A"#);
903 assert_first_letter_eq(r#"( "A"#, r#"( "A"#);
904 assert_first_letter_eq(r#"[ ( "A"#, r#"[ ( "A"#);
905
906 assert_first_letter_eq(r#"A""#, r#"A""#);
909 assert_first_letter_eq(r#"A" "#, r#"A""#);
910 assert_first_letter_eq(r#"A)]"#, r#"A)]"#);
911 assert_first_letter_eq(r#"A" )]"#, r#"A""#);
912 assert_first_letter_eq(r#"A)] >"#, r#"A)]"#);
913
914 assert_first_letter_eq(r#" ("A" )]"#, r#"("A""#);
916 assert_first_letter_eq(r#" ("A")] >"#, r#"("A")]"#);
917
918 assert_first_letter_eq("一", "一");
920 assert_first_letter_eq(" 一 ", "一");
921 assert_first_letter_eq("一二三", "一");
922 assert_first_letter_eq(" 一二三 ", "一");
923 assert_first_letter_eq("(一二三)", "(一");
924 assert_first_letter_eq(" (一二三) ", "(一");
925 assert_first_letter_eq("((一", "((一");
926 assert_first_letter_eq(" ( (一", "( (一");
927 assert_first_letter_eq("一)", "一)");
928 assert_first_letter_eq("一))", "一))");
929 assert_first_letter_eq("一) )", "一)");
930 }
931}