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 is_document_white_space(character: char) -> bool {
116 character.is_ascii_whitespace()
117 }
118
119 pub(crate) fn new(info: &NodeAndStyleInfo, context: &LayoutContext) -> Self {
120 Self {
121 on_word_boundary: true,
123 is_empty: true,
124 shared_inline_styles_stack: vec![SharedInlineStyles::from_info_and_context(
125 info, context,
126 )],
127 shared_selection: info.node.selection(),
128 ..Default::default()
129 }
130 }
131
132 pub(crate) fn currently_processing_inline_box(&self) -> bool {
133 !self.inline_box_stack.is_empty()
134 }
135
136 fn push_control_character_string(&mut self, string_to_push: &str) {
137 self.text_segments.push(string_to_push.to_owned());
138 self.current_text_offset += string_to_push.len();
139 self.current_character_offset += string_to_push.chars().count();
140 }
141
142 fn shared_inline_styles(&self) -> SharedInlineStyles {
143 self.shared_inline_styles_stack
144 .last()
145 .expect("Should always have at least one SharedInlineStyles")
146 .clone()
147 }
148
149 pub(crate) fn push_atomic(
150 &mut self,
151 independent_formatting_context_creator: impl FnOnce()
152 -> ArcRefCell<IndependentFormattingContext>,
153 old_layout_box: Option<LayoutBox>,
154 ) -> InlineItem {
155 let independent_formatting_context = old_layout_box
157 .and_then(|layout_box| match layout_box {
158 LayoutBox::InlineLevel(InlineItem::Atomic(atomic, ..)) => Some(atomic),
159 _ => None,
160 })
161 .unwrap_or_else(independent_formatting_context_creator);
162
163 let inline_level_box = InlineItem::Atomic(
164 independent_formatting_context,
165 self.current_text_offset,
166 Level::ltr(), );
168 self.inline_items.push(inline_level_box.clone());
169 self.is_empty = false;
170
171 self.push_control_character_string("\u{fffc}");
174
175 self.last_inline_box_ended_with_collapsible_white_space = false;
176 self.on_word_boundary = true;
177
178 self.has_processed_first_letter = true;
180
181 inline_level_box
182 }
183
184 pub(crate) fn push_absolutely_positioned_box(
185 &mut self,
186 absolutely_positioned_box_creator: impl FnOnce() -> ArcRefCell<AbsolutelyPositionedBox>,
187 old_layout_box: Option<LayoutBox>,
188 ) -> InlineItem {
189 let absolutely_positioned_box = old_layout_box
190 .and_then(|layout_box| match layout_box {
191 LayoutBox::InlineLevel(InlineItem::OutOfFlowAbsolutelyPositionedBox(
192 positioned_box,
193 ..,
194 )) => Some(positioned_box),
195 _ => None,
196 })
197 .unwrap_or_else(absolutely_positioned_box_creator);
198
199 let inline_level_box = InlineItem::OutOfFlowAbsolutelyPositionedBox(
201 absolutely_positioned_box,
202 self.current_text_offset,
203 );
204
205 self.inline_items.push(inline_level_box.clone());
206 self.is_empty = false;
207 inline_level_box
208 }
209
210 pub(crate) fn push_float_box(
211 &mut self,
212 float_box_creator: impl FnOnce() -> ArcRefCell<FloatBox>,
213 old_layout_box: Option<LayoutBox>,
214 ) -> InlineItem {
215 let inline_level_box = old_layout_box
216 .and_then(|layout_box| match layout_box {
217 LayoutBox::InlineLevel(inline_item) => Some(inline_item),
218 _ => None,
219 })
220 .unwrap_or_else(|| InlineItem::OutOfFlowFloatBox(float_box_creator()));
221
222 debug_assert!(
223 matches!(inline_level_box, InlineItem::OutOfFlowFloatBox(..),),
224 "Created float box with incompatible `old_layout_box`"
225 );
226
227 self.inline_items.push(inline_level_box.clone());
228 self.is_empty = false;
229 self.contains_floats = true;
230 inline_level_box
231 }
232
233 pub(crate) fn push_block_level_box(&mut self, block_level: ArcRefCell<BlockLevelBox>) {
234 assert!(self.currently_processing_inline_box());
235 self.contains_floats = self.contains_floats || block_level.borrow().contains_floats();
236 self.inline_items.push(InlineItem::BlockLevel(block_level));
237 }
238
239 pub(crate) fn start_inline_box(
240 &mut self,
241 inline_box_creator: impl FnOnce() -> ArcRefCell<InlineBox>,
242 old_layout_box: Option<LayoutBox>,
243 ) -> InlineItem {
244 let inline_box = old_layout_box
246 .and_then(|layout_box| match layout_box {
247 LayoutBox::InlineLevel(InlineItem::StartInlineBox(inline_box)) => Some(inline_box),
248 _ => None,
249 })
250 .unwrap_or_else(inline_box_creator);
251
252 let borrowed_inline_box = inline_box.borrow();
253 self.push_control_character_string(borrowed_inline_box.base.style.bidi_control_chars().0);
254
255 self.shared_inline_styles_stack
256 .push(borrowed_inline_box.shared_inline_styles.clone());
257 std::mem::drop(borrowed_inline_box);
258
259 let identifier = self.inline_boxes.start_inline_box(inline_box.clone());
260 let inline_item = InlineItem::StartInlineBox(inline_box);
261 self.inline_items.push(inline_item.clone());
262 self.inline_box_stack.push(identifier);
263 self.is_empty = false;
264 inline_item
265 }
266
267 pub(crate) fn end_inline_box(&mut self) {
272 self.shared_inline_styles_stack.pop();
273 self.inline_items.push(InlineItem::EndInlineBox);
274 let identifier = self
275 .inline_box_stack
276 .pop()
277 .expect("Ended non-existent inline box");
278 self.inline_boxes.end_inline_box(identifier);
279 let inline_level_box = self.inline_boxes.get(&identifier);
280 let bidi_control_chars = inline_level_box.borrow().base.style.bidi_control_chars();
281 self.push_control_character_string(bidi_control_chars.1);
282 }
283
284 pub(crate) fn push_text_with_possible_first_letter<'dom>(
292 &mut self,
293 text: Cow<'dom, str>,
294 info: &NodeAndStyleInfo<'dom>,
295 container_info: &NodeAndStyleInfo<'dom>,
296 layout_context: &LayoutContext,
297 ) -> bool {
298 if self.has_processed_first_letter || !container_info.pseudo_element_chain().is_empty() {
299 self.push_text(text, info);
300 return false;
301 }
302
303 let Some(first_letter_info) =
304 container_info.with_pseudo_element(layout_context, PseudoElement::FirstLetter)
305 else {
306 self.push_text(text, info);
307 return false;
308 };
309
310 let first_letter_range = first_letter_range(&text[..]);
311 if first_letter_range.is_empty() {
312 return false;
313 }
314
315 if first_letter_range.start != 0 {
317 self.push_text(Cow::Borrowed(&text[0..first_letter_range.start]), info);
318 }
319
320 let box_slot = first_letter_info.node.box_slot();
322 let inline_item = self.start_inline_box(
323 || ArcRefCell::new(InlineBox::new(&first_letter_info, layout_context)),
324 None,
325 );
326 box_slot.set(LayoutBox::InlineLevel(inline_item));
327
328 let first_letter_text = Cow::Borrowed(&text[first_letter_range.clone()]);
329 self.push_text(first_letter_text, &first_letter_info);
330 self.end_inline_box();
331 self.has_processed_first_letter = true;
332
333 self.push_text(Cow::Borrowed(&text[first_letter_range.end..]), info);
335
336 true
337 }
338
339 pub(crate) fn push_text<'dom>(&mut self, text: Cow<'dom, str>, info: &NodeAndStyleInfo<'dom>) {
340 let white_space_collapse = info.style.clone_white_space_collapse();
341 let collapsed = WhitespaceCollapse::new(
342 text.chars(),
343 white_space_collapse,
344 self.last_inline_box_ended_with_collapsible_white_space,
345 );
346
347 let text_transform = info.style.clone_text_transform().case();
350 let capitalized_text: String;
351 let char_iterator: Box<dyn Iterator<Item = char>> = match text_transform {
352 TextTransformCase::None => Box::new(collapsed),
353 TextTransformCase::Capitalize => {
354 let collapsed_string: String = collapsed.collect();
361 capitalized_text = capitalize_string(&collapsed_string, self.on_word_boundary);
362 Box::new(capitalized_text.chars())
363 },
364 _ => {
365 Box::new(TextTransformation::new(collapsed, text_transform))
368 },
369 };
370
371 let char_iterator = if info.style.clone__webkit_text_security() != WebKitTextSecurity::None
372 {
373 Box::new(TextSecurityTransform::new(
374 char_iterator,
375 info.style.clone__webkit_text_security(),
376 ))
377 } else {
378 char_iterator
379 };
380
381 let white_space_collapse = info.style.clone_white_space_collapse();
382 let mut character_count = 0;
383 let new_text: String = char_iterator
384 .inspect(|&character| {
385 character_count += 1;
386
387 self.is_empty = self.is_empty &&
388 match white_space_collapse {
389 WhiteSpaceCollapse::Collapse => Self::is_document_white_space(character),
390 WhiteSpaceCollapse::PreserveBreaks => {
391 Self::is_document_white_space(character) && character != '\n'
392 },
393 WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces => false,
394 };
395 })
396 .collect();
397
398 if new_text.is_empty() {
399 return;
400 }
401
402 if let Some(last_character) = new_text.chars().next_back() {
403 self.on_word_boundary = last_character.is_whitespace();
404 self.last_inline_box_ended_with_collapsible_white_space =
405 self.on_word_boundary && white_space_collapse != WhiteSpaceCollapse::Preserve;
406 }
407
408 let new_range = self.current_text_offset..self.current_text_offset + new_text.len();
409 self.current_text_offset = new_range.end;
410
411 let new_character_range =
412 self.current_character_offset..self.current_character_offset + character_count;
413 self.current_character_offset = new_character_range.end;
414
415 self.text_segments.push(new_text);
416
417 let current_inline_styles = self.shared_inline_styles();
418
419 if let Some(InlineItem::TextRun(text_run)) = self.inline_items.last() {
420 if text_run
421 .borrow()
422 .inline_styles
423 .ptr_eq(¤t_inline_styles)
424 {
425 let box_slot = info.node.box_slot();
426 let old_text_run = box_slot.take_layout_box_as_text_run();
427
428 {
429 let mut text_run = text_run.borrow_mut();
430 text_run.text_range.end = new_range.end;
431 text_run.character_range.end = new_character_range.end;
432
433 if old_text_run.is_none() {
438 text_run.items.clear();
439 }
440 }
441
442 box_slot.set(LayoutBox::Text(text_run.clone()));
443 return;
444 }
445 }
446
447 let box_slot = info.node.box_slot();
448 let text_run = ArcRefCell::new(TextRun::new(
449 info.into(),
450 current_inline_styles,
451 new_range,
452 new_character_range,
453 box_slot.take_layout_box_as_text_run(),
454 ));
455 self.inline_items
456 .push(InlineItem::TextRun(text_run.clone()));
457 box_slot.set(LayoutBox::Text(text_run));
458 }
459
460 pub(crate) fn enter_display_contents(&mut self, shared_inline_styles: SharedInlineStyles) {
461 self.shared_inline_styles_stack.push(shared_inline_styles);
462 }
463
464 pub(crate) fn leave_display_contents(&mut self) {
465 self.shared_inline_styles_stack.pop();
466 }
467
468 pub(crate) fn finish(
470 self,
471 layout_context: &LayoutContext,
472 has_first_formatted_line: bool,
473 is_single_line_text_input: bool,
474 default_bidi_level: Level,
475 ) -> Option<InlineFormattingContext> {
476 if self.is_empty {
477 return None;
478 }
479
480 assert!(self.inline_box_stack.is_empty());
481 Some(InlineFormattingContext::new_with_builder(
482 self,
483 layout_context,
484 has_first_formatted_line,
485 is_single_line_text_input,
486 default_bidi_level,
487 ))
488 }
489}
490
491fn preserve_segment_break() -> bool {
492 true
493}
494
495pub struct WhitespaceCollapse<InputIterator> {
496 char_iterator: InputIterator,
497 white_space_collapse: WhiteSpaceCollapse,
498
499 remove_collapsible_white_space_at_start: bool,
503
504 following_newline: bool,
507
508 have_seen_non_white_space_characters: bool,
511
512 inside_white_space: bool,
516
517 character_pending_to_return: Option<char>,
521}
522
523impl<InputIterator> WhitespaceCollapse<InputIterator> {
524 pub fn new(
525 char_iterator: InputIterator,
526 white_space_collapse: WhiteSpaceCollapse,
527 trim_beginning_white_space: bool,
528 ) -> Self {
529 Self {
530 char_iterator,
531 white_space_collapse,
532 remove_collapsible_white_space_at_start: trim_beginning_white_space,
533 inside_white_space: false,
534 following_newline: false,
535 have_seen_non_white_space_characters: false,
536 character_pending_to_return: None,
537 }
538 }
539
540 fn is_leading_trimmed_white_space(&self) -> bool {
541 !self.have_seen_non_white_space_characters && self.remove_collapsible_white_space_at_start
542 }
543
544 fn need_to_produce_space_character_after_white_space(&self) -> bool {
549 self.inside_white_space && !self.following_newline && !self.is_leading_trimmed_white_space()
550 }
551}
552
553impl<InputIterator> Iterator for WhitespaceCollapse<InputIterator>
554where
555 InputIterator: Iterator<Item = char>,
556{
557 type Item = char;
558
559 fn next(&mut self) -> Option<Self::Item> {
560 if self.white_space_collapse == WhiteSpaceCollapse::Preserve ||
566 self.white_space_collapse == WhiteSpaceCollapse::BreakSpaces
567 {
568 return match self.char_iterator.next() {
573 Some('\r') => Some(' '),
574 next => next,
575 };
576 }
577
578 if let Some(character) = self.character_pending_to_return.take() {
579 self.inside_white_space = false;
580 self.have_seen_non_white_space_characters = true;
581 self.following_newline = false;
582 return Some(character);
583 }
584
585 while let Some(character) = self.char_iterator.next() {
586 if InlineFormattingContextBuilder::is_document_white_space(character) &&
590 character != '\n'
591 {
592 self.inside_white_space = true;
593 continue;
594 }
595
596 if character == '\n' {
600 if self.white_space_collapse != WhiteSpaceCollapse::Collapse {
606 self.inside_white_space = false;
607 self.following_newline = true;
608 return Some(character);
609
610 } else if !self.following_newline &&
616 preserve_segment_break() &&
617 !self.is_leading_trimmed_white_space()
618 {
619 self.inside_white_space = false;
620 self.following_newline = true;
621 return Some(' ');
622 } else {
623 self.following_newline = true;
624 continue;
625 }
626 }
627
628 if self.need_to_produce_space_character_after_white_space() {
637 self.inside_white_space = false;
638 self.character_pending_to_return = Some(character);
639 return Some(' ');
640 }
641
642 self.inside_white_space = false;
643 self.have_seen_non_white_space_characters = true;
644 self.following_newline = false;
645 return Some(character);
646 }
647
648 if self.need_to_produce_space_character_after_white_space() {
649 self.inside_white_space = false;
650 return Some(' ');
651 }
652
653 None
654 }
655
656 fn size_hint(&self) -> (usize, Option<usize>) {
657 self.char_iterator.size_hint()
658 }
659
660 fn count(self) -> usize
661 where
662 Self: Sized,
663 {
664 self.char_iterator.count()
665 }
666}
667
668enum PendingCaseConversionResult {
669 Uppercase(ToUppercase),
670 Lowercase(ToLowercase),
671}
672
673impl PendingCaseConversionResult {
674 fn next(&mut self) -> Option<char> {
675 match self {
676 PendingCaseConversionResult::Uppercase(to_uppercase) => to_uppercase.next(),
677 PendingCaseConversionResult::Lowercase(to_lowercase) => to_lowercase.next(),
678 }
679 }
680}
681
682pub struct TextTransformation<InputIterator> {
687 char_iterator: InputIterator,
689 text_transform: TextTransformCase,
691 pending_case_conversion_result: Option<PendingCaseConversionResult>,
694}
695
696impl<InputIterator> TextTransformation<InputIterator> {
697 pub fn new(char_iterator: InputIterator, text_transform: TextTransformCase) -> Self {
698 Self {
699 char_iterator,
700 text_transform,
701 pending_case_conversion_result: None,
702 }
703 }
704}
705
706impl<InputIterator> Iterator for TextTransformation<InputIterator>
707where
708 InputIterator: Iterator<Item = char>,
709{
710 type Item = char;
711
712 fn next(&mut self) -> Option<Self::Item> {
713 if let Some(character) = self
714 .pending_case_conversion_result
715 .as_mut()
716 .and_then(|result| result.next())
717 {
718 return Some(character);
719 }
720 self.pending_case_conversion_result = None;
721
722 for character in self.char_iterator.by_ref() {
723 match self.text_transform {
724 TextTransformCase::None => return Some(character),
725 TextTransformCase::Uppercase => {
726 let mut pending_result =
727 PendingCaseConversionResult::Uppercase(character.to_uppercase());
728 if let Some(character) = pending_result.next() {
729 self.pending_case_conversion_result = Some(pending_result);
730 return Some(character);
731 }
732 },
733 TextTransformCase::Lowercase => {
734 let mut pending_result =
735 PendingCaseConversionResult::Lowercase(character.to_lowercase());
736 if let Some(character) = pending_result.next() {
737 self.pending_case_conversion_result = Some(pending_result);
738 return Some(character);
739 }
740 },
741 TextTransformCase::Capitalize => return Some(character),
744 }
745 }
746 None
747 }
748}
749
750pub struct TextSecurityTransform<InputIterator> {
751 char_iterator: InputIterator,
753 text_security: WebKitTextSecurity,
755}
756
757impl<InputIterator> TextSecurityTransform<InputIterator> {
758 pub fn new(char_iterator: InputIterator, text_security: WebKitTextSecurity) -> Self {
759 Self {
760 char_iterator,
761 text_security,
762 }
763 }
764}
765
766impl<InputIterator> Iterator for TextSecurityTransform<InputIterator>
767where
768 InputIterator: Iterator<Item = char>,
769{
770 type Item = char;
771
772 fn next(&mut self) -> Option<Self::Item> {
773 Some(match self.char_iterator.next()? {
777 '\u{200B}' => '\u{200B}',
781 '\n' => '\n',
783 character => match self.text_security {
784 WebKitTextSecurity::None => character,
785 WebKitTextSecurity::Circle => '○',
786 WebKitTextSecurity::Disc => '●',
787 WebKitTextSecurity::Square => '■',
788 },
789 })
790 }
791}
792
793pub(crate) fn capitalize_string(string: &str, allow_word_at_start: bool) -> String {
796 let mut output_string = String::new();
797 output_string.reserve(string.len());
798
799 let word_segmenter = WordSegmenter::new_auto();
800 let mut bounds = word_segmenter.segment_str(string).peekable();
801 let mut byte_index = 0;
802 for character in string.chars() {
803 let current_byte_index = byte_index;
804 byte_index += character.len_utf8();
805
806 if let Some(next_index) = bounds.peek() {
807 if *next_index == current_byte_index {
808 bounds.next();
809
810 if current_byte_index != 0 || allow_word_at_start {
811 output_string.extend(character.to_uppercase());
812 continue;
813 }
814 }
815 }
816
817 output_string.push(character);
818 }
819
820 output_string
821}
822
823fn first_letter_range(text: &str) -> Range<usize> {
833 enum State {
834 Start,
836 PrecedingPunctuation,
838 Lns,
840 TrailingPunctuation,
843 }
844
845 let mut start = 0;
846 let mut state = State::Start;
847 for (index, character) in text.char_indices() {
848 match &mut state {
849 State::Start => {
850 if character.is_letter() || character.is_number() || character.is_symbol() {
851 start = index;
852 state = State::Lns;
853 } else if character.is_punctuation() {
854 start = index;
855 state = State::PrecedingPunctuation
856 }
857 },
858 State::PrecedingPunctuation => {
859 if character.is_letter() || character.is_number() || character.is_symbol() {
860 state = State::Lns;
861 } else if !character.is_separator_space() && !character.is_punctuation() {
862 return 0..0;
863 }
864 },
865 State::Lns => {
866 if character.is_punctuation() &&
869 !character.is_punctuation_open() &&
870 !character.is_punctuation_dash()
871 {
872 state = State::TrailingPunctuation;
873 } else {
874 return start..index;
875 }
876 },
877 State::TrailingPunctuation => {
878 if character.is_punctuation() &&
881 !character.is_punctuation_open() &&
882 !character.is_punctuation_dash()
883 {
884 continue;
885 } else {
886 return start..index;
887 }
888 },
889 }
890 }
891
892 match state {
893 State::Start | State::PrecedingPunctuation => 0..0,
894 State::Lns | State::TrailingPunctuation => start..text.len(),
895 }
896}
897
898#[cfg(test)]
899mod tests {
900 use super::*;
901
902 fn assert_first_letter_eq(text: &str, expected: &str) {
903 let range = first_letter_range(text);
904 assert_eq!(&text[range], expected);
905 }
906
907 #[test]
908 fn test_first_letter_range() {
909 assert_first_letter_eq("", "");
911 assert_first_letter_eq(" ", "");
912
913 assert_first_letter_eq("(", "");
915 assert_first_letter_eq(" (", "");
916 assert_first_letter_eq("( ", "");
917 assert_first_letter_eq("()", "");
918
919 assert_first_letter_eq("\u{0903}", "");
921
922 assert_first_letter_eq("A", "A");
924 assert_first_letter_eq(" A", "A");
925 assert_first_letter_eq("A ", "A");
926 assert_first_letter_eq(" A ", "A");
927
928 assert_first_letter_eq("App", "A");
930 assert_first_letter_eq(" App", "A");
931 assert_first_letter_eq("App ", "A");
932
933 assert_first_letter_eq(r#""A"#, r#""A"#);
935 assert_first_letter_eq(r#" "A"#, r#""A"#);
936 assert_first_letter_eq(r#""A "#, r#""A"#);
937 assert_first_letter_eq(r#"" A"#, r#"" A"#);
938 assert_first_letter_eq(r#" "A "#, r#""A"#);
939 assert_first_letter_eq(r#"("A"#, r#"("A"#);
940 assert_first_letter_eq(r#" ("A"#, r#"("A"#);
941 assert_first_letter_eq(r#"( "A"#, r#"( "A"#);
942 assert_first_letter_eq(r#"[ ( "A"#, r#"[ ( "A"#);
943
944 assert_first_letter_eq(r#"A""#, r#"A""#);
947 assert_first_letter_eq(r#"A" "#, r#"A""#);
948 assert_first_letter_eq(r#"A)]"#, r#"A)]"#);
949 assert_first_letter_eq(r#"A" )]"#, r#"A""#);
950 assert_first_letter_eq(r#"A)] >"#, r#"A)]"#);
951
952 assert_first_letter_eq(r#" ("A" )]"#, r#"("A""#);
954 assert_first_letter_eq(r#" ("A")] >"#, r#"("A")]"#);
955
956 assert_first_letter_eq("一", "一");
958 assert_first_letter_eq(" 一 ", "一");
959 assert_first_letter_eq("一二三", "一");
960 assert_first_letter_eq(" 一二三 ", "一");
961 assert_first_letter_eq("(一二三)", "(一");
962 assert_first_letter_eq(" (一二三) ", "(一");
963 assert_first_letter_eq("((一", "((一");
964 assert_first_letter_eq(" ( (一", "( (一");
965 assert_first_letter_eq("一)", "一)");
966 assert_first_letter_eq("一))", "一))");
967 assert_first_letter_eq("一) )", "一)");
968 }
969}