1use std::borrow::Cow;
6use std::cell::LazyCell;
7use std::char::{ToLowercase, ToUppercase};
8use std::ops::{ControlFlow, Range};
9
10use icu_properties::BidiClass;
11use icu_segmenter::WordSegmenter;
12use layout_api::{LayoutNode, SharedSelection};
13use servo_base::text::Utf32CodeUnits;
14use style::computed_values::_webkit_text_security::T as WebKitTextSecurity;
15use style::computed_values::direction::T as Direction;
16use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
17use style::dom::NodeInfo;
18use style::selector_parser::PseudoElement;
19use style::values::specified::text::TextTransformCase;
20use unicode_bidi::Level;
21use unicode_categories::UnicodeCategories;
22
23use super::text_run::TextRun;
24use super::{
25 InlineBox, InlineBoxIdentifier, InlineBoxes, InlineFormattingContext, InlineItem,
26 SharedInlineStyles,
27};
28use crate::cell::ArcRefCell;
29use crate::context::LayoutContext;
30use crate::dom::{LayoutBox, NodeExt};
31use crate::dom_traversal::{BoxTreeString, NodeAndStyleInfo};
32use crate::flow::BlockLevelBox;
33use crate::flow::float::FloatBox;
34use crate::formatting_contexts::IndependentFormattingContext;
35use crate::positioned::AbsolutelyPositionedBox;
36use crate::style_ext::ComputedValuesExt;
37
38#[derive(Default)]
39pub(crate) struct InlineFormattingContextBuilder {
40 pub shared_inline_styles_stack: Vec<SharedInlineStyles>,
45
46 pub text_segments: Vec<String>,
49
50 current_text_offset: usize,
53
54 current_character_offset: usize,
58
59 pub shared_selection: Option<SharedSelection>,
62
63 last_inline_box_ended_with_collapsible_white_space: bool,
71
72 on_word_boundary: bool,
75
76 pub contains_floats: bool,
78
79 pub inline_items: Vec<InlineItem>,
83
84 pub inline_boxes: InlineBoxes,
86
87 inline_box_stack: Vec<InlineBoxIdentifier>,
96
97 pub is_empty: bool,
101
102 has_processed_first_letter: bool,
105
106 pub(crate) has_right_to_left_content: bool,
110}
111
112impl InlineFormattingContextBuilder {
113 pub(crate) fn is_document_white_space(character: char) -> bool {
126 character.is_ascii_whitespace()
127 }
128
129 pub(crate) fn new(info: &NodeAndStyleInfo, context: &LayoutContext) -> Self {
130 let has_right_to_left_content = info.style.get_inherited_box().direction == Direction::Rtl;
131 Self {
132 on_word_boundary: true,
134 is_empty: true,
135 shared_inline_styles_stack: vec![SharedInlineStyles::from_info_and_context(
136 info, context,
137 )],
138 shared_selection: info.node.selection(),
139 has_right_to_left_content,
140 ..Default::default()
141 }
142 }
143
144 pub(crate) fn currently_processing_inline_box(&self) -> bool {
145 !self.inline_box_stack.is_empty()
146 }
147
148 fn push_control_character_string(&mut self, string_to_push: &str) {
149 self.text_segments.push(string_to_push.to_owned());
150 self.current_text_offset += string_to_push.len();
151 self.current_character_offset += string_to_push.chars().count();
152 }
153
154 fn shared_inline_styles(&self) -> SharedInlineStyles {
155 self.shared_inline_styles_stack
156 .last()
157 .expect("Should always have at least one SharedInlineStyles")
158 .clone()
159 }
160
161 pub(crate) fn push_atomic(
162 &mut self,
163 independent_formatting_context_creator: impl FnOnce()
164 -> ArcRefCell<IndependentFormattingContext>,
165 old_layout_box: Option<LayoutBox>,
166 ) -> InlineItem {
167 let independent_formatting_context = old_layout_box
169 .and_then(|layout_box| match layout_box {
170 LayoutBox::InlineLevel(InlineItem::Atomic(atomic, ..)) => Some(atomic),
171 _ => None,
172 })
173 .unwrap_or_else(independent_formatting_context_creator);
174
175 let inline_level_box = InlineItem::Atomic(
176 independent_formatting_context,
177 self.current_text_offset,
178 Level::ltr(), );
180 self.inline_items.push(inline_level_box.clone());
181 self.is_empty = false;
182
183 self.push_control_character_string("\u{fffc}");
186
187 self.last_inline_box_ended_with_collapsible_white_space = false;
188 self.on_word_boundary = true;
189
190 self.has_processed_first_letter = true;
192
193 inline_level_box
194 }
195
196 pub(crate) fn push_absolutely_positioned_box(
197 &mut self,
198 absolutely_positioned_box_creator: impl FnOnce() -> ArcRefCell<AbsolutelyPositionedBox>,
199 old_layout_box: Option<LayoutBox>,
200 ) -> InlineItem {
201 let absolutely_positioned_box = old_layout_box
202 .and_then(|layout_box| match layout_box {
203 LayoutBox::InlineLevel(InlineItem::OutOfFlowAbsolutelyPositionedBox(
204 positioned_box,
205 ..,
206 )) => Some(positioned_box),
207 _ => None,
208 })
209 .unwrap_or_else(absolutely_positioned_box_creator);
210
211 let inline_level_box = InlineItem::OutOfFlowAbsolutelyPositionedBox(
213 absolutely_positioned_box,
214 self.current_text_offset,
215 );
216
217 self.inline_items.push(inline_level_box.clone());
218 self.is_empty = false;
219 inline_level_box
220 }
221
222 pub(crate) fn push_float_box(
223 &mut self,
224 float_box_creator: impl FnOnce() -> ArcRefCell<FloatBox>,
225 old_layout_box: Option<LayoutBox>,
226 ) -> InlineItem {
227 let inline_level_box = old_layout_box
228 .and_then(|layout_box| match layout_box {
229 LayoutBox::InlineLevel(inline_item) => Some(inline_item),
230 _ => None,
231 })
232 .unwrap_or_else(|| InlineItem::OutOfFlowFloatBox(float_box_creator()));
233
234 debug_assert!(
235 matches!(inline_level_box, InlineItem::OutOfFlowFloatBox(..),),
236 "Created float box with incompatible `old_layout_box`"
237 );
238
239 self.inline_items.push(inline_level_box.clone());
240 self.is_empty = false;
241 self.contains_floats = true;
242 inline_level_box
243 }
244
245 pub(crate) fn push_block_level_box(&mut self, block_level: ArcRefCell<BlockLevelBox>) {
246 assert!(self.currently_processing_inline_box());
247 self.contains_floats = self.contains_floats || block_level.borrow().contains_floats();
248 self.inline_items.push(InlineItem::BlockLevel(block_level));
249 }
250
251 pub(crate) fn start_inline_box(
252 &mut self,
253 inline_box_creator: impl FnOnce() -> ArcRefCell<InlineBox>,
254 old_layout_box: Option<LayoutBox>,
255 ) -> InlineItem {
256 let inline_box = old_layout_box
258 .and_then(|layout_box| match layout_box {
259 LayoutBox::InlineLevel(InlineItem::StartInlineBox(inline_box)) => Some(inline_box),
260 _ => None,
261 })
262 .unwrap_or_else(inline_box_creator);
263
264 let borrowed_inline_box = inline_box.borrow();
265
266 let style = &borrowed_inline_box.base.style;
267 self.push_control_character_string(style.bidi_control_chars().0);
268 self.has_right_to_left_content =
269 self.has_right_to_left_content || style.get_inherited_box().direction == Direction::Rtl;
270
271 self.shared_inline_styles_stack
272 .push(borrowed_inline_box.shared_inline_styles.clone());
273 std::mem::drop(borrowed_inline_box);
274
275 let identifier = self.inline_boxes.start_inline_box(inline_box.clone());
276 let inline_item = InlineItem::StartInlineBox(inline_box);
277 self.inline_items.push(inline_item.clone());
278 self.inline_box_stack.push(identifier);
279 self.is_empty = false;
280 inline_item
281 }
282
283 pub(crate) fn end_inline_box(&mut self) {
288 let identifier = self
289 .inline_box_stack
290 .pop()
291 .expect("Ended non-existent inline box");
292 let inline_level_box = self.inline_boxes.get(&identifier);
293
294 self.shared_inline_styles_stack.pop();
295 self.inline_items
296 .push(InlineItem::EndInlineBox(inline_level_box.clone()));
297 self.inline_boxes.end_inline_box(identifier);
298 let bidi_control_chars = inline_level_box.borrow().base.style.bidi_control_chars();
299 self.push_control_character_string(bidi_control_chars.1);
300 }
301
302 pub(crate) fn push_text_with_possible_first_letter<'dom>(
310 &mut self,
311 text: BoxTreeString<'dom>,
312 info: &NodeAndStyleInfo<'dom>,
313 container_info: &NodeAndStyleInfo<'dom>,
314 layout_context: &LayoutContext,
315 ) -> bool {
316 let document_selection = info.node.document_selection_in_text_node();
317 if self.has_processed_first_letter || !container_info.pseudo_element_chain().is_empty() {
318 self.push_text(text, info, document_selection);
319 return false;
320 }
321
322 let Some(first_letter_info) =
323 container_info.with_pseudo_element(layout_context, PseudoElement::FirstLetter)
324 else {
325 self.push_text(text, info, document_selection);
326 return false;
327 };
328
329 let first_letter_range = first_letter_range(&text[..]);
330 if first_letter_range.is_empty() {
331 return false;
332 }
333
334 let intersect_ranges = |a: Range<Utf32CodeUnits>, b: Range<Utf32CodeUnits>| {
335 let start = a.start.max(b.start);
336 let end = b.end.min(b.end);
337 if start < end { Some(start..end) } else { None }
338 };
339
340 let first_letter_range_u32 = LazyCell::new(|| {
342 Utf32CodeUnits::length_of(&text[..first_letter_range.start])..
343 Utf32CodeUnits::length_of(&text[..first_letter_range.end])
344 });
345 if first_letter_range.start != 0 {
346 let leading_whitespace_range = 0..first_letter_range.start;
347 let leading_whitespace_selection_range =
348 document_selection.clone().and_then(|document_selection| {
349 let leading_whitespace_range_u32 =
350 Utf32CodeUnits::zero()..first_letter_range_u32.start;
351 intersect_ranges(document_selection, leading_whitespace_range_u32)
352 });
353
354 self.push_text(
355 Cow::Borrowed(&text[leading_whitespace_range]).into(),
356 info,
357 leading_whitespace_selection_range,
358 );
359 }
360
361 let box_slot = first_letter_info.node.box_slot();
363 let inline_item = self.start_inline_box(
364 || ArcRefCell::new(InlineBox::new(&first_letter_info, layout_context)),
365 None,
366 );
367 box_slot.set(LayoutBox::InlineLevel(inline_item));
368
369 let first_letter_text = Cow::Borrowed(&text[first_letter_range.clone()]);
370 let first_letter_selection_range =
371 document_selection.clone().and_then(|document_selection| {
372 intersect_ranges(document_selection, (*first_letter_range_u32).clone()).map(
373 |range| {
374 range.start - first_letter_range_u32.start..
375 range.end - first_letter_range_u32.start
376 },
377 )
378 });
379 self.push_text(
380 first_letter_text.into(),
381 &first_letter_info,
382 first_letter_selection_range,
383 );
384 self.end_inline_box();
385 self.has_processed_first_letter = true;
386
387 let remaining_selection_range = document_selection.and_then(|document_selection| {
389 let remaining_text_range_u32 = first_letter_range_u32.end..document_selection.end;
390 intersect_ranges(document_selection, remaining_text_range_u32).map(|range| {
391 range.start - first_letter_range_u32.end..range.end - first_letter_range_u32.end
392 })
393 });
394 self.push_text(
395 Cow::Borrowed(&text[first_letter_range.end..]).into(),
396 info,
397 remaining_selection_range,
398 );
399
400 true
401 }
402
403 pub(crate) fn push_text<'dom>(
404 &mut self,
405 text: BoxTreeString<'dom>,
406 info: &NodeAndStyleInfo<'dom>,
407 document_selection: Option<Range<Utf32CodeUnits>>,
408 ) {
409 let white_space_collapse = info.style.clone_white_space_collapse();
410 let collapsed = WhitespaceCollapse::new(
411 text.chars(),
412 white_space_collapse,
413 self.last_inline_box_ended_with_collapsible_white_space,
414 );
415
416 let text_transform = info.style.clone_text_transform().case();
419 let capitalized_text: String;
420 let char_iterator: Box<dyn Iterator<Item = char>> = match text_transform {
421 TextTransformCase::None => Box::new(collapsed),
422 TextTransformCase::Capitalize => {
423 let collapsed_string: String = collapsed.collect();
430 capitalized_text = capitalize_string(&collapsed_string, self.on_word_boundary);
431 Box::new(capitalized_text.chars())
432 },
433 _ => {
434 Box::new(TextTransformation::new(collapsed, text_transform))
437 },
438 };
439
440 let char_iterator = if info.style.clone__webkit_text_security() != WebKitTextSecurity::None
441 {
442 Box::new(TextSecurityTransform::new(
443 char_iterator,
444 info.style.clone__webkit_text_security(),
445 ))
446 } else {
447 char_iterator
448 };
449
450 let bidi_class_map = icu_properties::maps::bidi_class();
451 let white_space_collapse = info.style.clone_white_space_collapse();
452 let mut character_count = 0;
453 let new_text: String = char_iterator
454 .inspect(|&character| {
455 character_count += 1;
456
457 self.has_right_to_left_content = self.has_right_to_left_content ||
461 matches!(
462 bidi_class_map.get(character),
463 BidiClass::RightToLeft |
464 BidiClass::ArabicLetter |
465 BidiClass::RightToLeftEmbedding |
466 BidiClass::RightToLeftIsolate |
467 BidiClass::RightToLeftOverride
468 );
469
470 self.is_empty = self.is_empty &&
471 match white_space_collapse {
472 WhiteSpaceCollapse::Collapse => Self::is_document_white_space(character),
473 WhiteSpaceCollapse::PreserveBreaks => {
474 Self::is_document_white_space(character) && character != '\n'
475 },
476 WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces => false,
477 };
478 })
479 .collect();
480
481 if new_text.is_empty() {
482 return;
483 }
484
485 if let Some(last_character) = new_text.chars().next_back() {
486 self.on_word_boundary = last_character.is_whitespace();
487 self.last_inline_box_ended_with_collapsible_white_space =
488 self.on_word_boundary && white_space_collapse != WhiteSpaceCollapse::Preserve;
489 }
490
491 let new_utf8_range = self.current_text_offset..self.current_text_offset + new_text.len();
492 self.current_text_offset = new_utf8_range.end;
493
494 let new_character_range =
495 self.current_character_offset..self.current_character_offset + character_count;
496 self.current_character_offset = new_character_range.end;
497
498 self.text_segments.push(new_text);
499
500 if self
501 .try_to_push_text_range_to_previous_text_run(
502 info,
503 &document_selection,
504 &new_utf8_range,
505 &new_character_range,
506 )
507 .is_break()
508 {
509 return;
510 }
511
512 let current_inline_styles = self.shared_inline_styles();
513 let box_slot = info.node.is_text_node().then(|| info.node.box_slot());
514 let text_run = ArcRefCell::new(TextRun::new(
515 info.into(),
516 current_inline_styles,
517 new_utf8_range,
518 new_character_range,
519 document_selection.unwrap_or_default(),
520 box_slot
521 .as_ref()
522 .and_then(|box_slot| box_slot.take_layout_box_as_text_run()),
523 ));
524 self.inline_items
525 .push(InlineItem::TextRun(text_run.clone()));
526
527 if let Some(box_slot) = box_slot {
528 box_slot.set(LayoutBox::Text(text_run));
529 }
530 }
531
532 fn try_to_push_text_range_to_previous_text_run(
533 &mut self,
534 info: &NodeAndStyleInfo,
535 new_text_selection: &Option<Range<Utf32CodeUnits>>,
536 new_range: &Range<usize>,
537 new_character_range: &Range<usize>,
538 ) -> ControlFlow<()> {
539 let Some(InlineItem::TextRun(text_run_arc)) = self.inline_items.last() else {
541 return ControlFlow::Continue(());
542 };
543
544 if !text_run_arc
546 .borrow()
547 .inline_styles
548 .ptr_eq(&self.shared_inline_styles())
549 {
550 return ControlFlow::Continue(());
551 }
552
553 let mut text_run = text_run_arc.borrow_mut();
554 if let Some(next_text_selection) = new_text_selection {
555 let existing_characters = text_run.character_range.end - text_run.character_range.start;
556 if !text_run.document_selection.is_empty() {
557 if text_run.document_selection.end.0 == existing_characters {
560 text_run.document_selection.end += next_text_selection.end;
561 } else {
562 return ControlFlow::Continue(());
563 }
564 } else {
565 text_run.document_selection = Utf32CodeUnits(existing_characters) +
567 next_text_selection.start..
568 Utf32CodeUnits(existing_characters) + next_text_selection.end;
569 }
570 }
571
572 text_run.text_range.end = new_range.end;
573 text_run.character_range.end = new_character_range.end;
574
575 let box_slot = info.node.box_slot();
580 let old_text_run = box_slot.take_layout_box_as_text_run();
581 if old_text_run.is_none() {
582 text_run.items.clear();
583 }
584
585 box_slot.set(LayoutBox::Text(text_run_arc.clone()));
586 ControlFlow::Break(())
587 }
588
589 pub(crate) fn enter_display_contents(&mut self, shared_inline_styles: SharedInlineStyles) {
590 self.shared_inline_styles_stack.push(shared_inline_styles);
591 }
592
593 pub(crate) fn leave_display_contents(&mut self) {
594 self.shared_inline_styles_stack.pop();
595 }
596
597 pub(crate) fn finish(
599 self,
600 layout_context: &LayoutContext,
601 has_first_formatted_line: bool,
602 is_single_line_text_input: bool,
603 default_bidi_level: Level,
604 ) -> Option<InlineFormattingContext> {
605 if self.is_empty {
606 return None;
607 }
608
609 assert!(self.inline_box_stack.is_empty());
610 Some(InlineFormattingContext::new_with_builder(
611 self,
612 layout_context,
613 has_first_formatted_line,
614 is_single_line_text_input,
615 default_bidi_level,
616 ))
617 }
618}
619
620fn preserve_segment_break() -> bool {
621 true
622}
623
624pub struct WhitespaceCollapse<InputIterator> {
625 char_iterator: InputIterator,
626 white_space_collapse: WhiteSpaceCollapse,
627
628 remove_collapsible_white_space_at_start: bool,
632
633 following_newline: bool,
636
637 have_seen_non_white_space_characters: bool,
640
641 inside_white_space: bool,
645
646 character_pending_to_return: Option<char>,
650}
651
652impl<InputIterator> WhitespaceCollapse<InputIterator> {
653 pub fn new(
654 char_iterator: InputIterator,
655 white_space_collapse: WhiteSpaceCollapse,
656 trim_beginning_white_space: bool,
657 ) -> Self {
658 Self {
659 char_iterator,
660 white_space_collapse,
661 remove_collapsible_white_space_at_start: trim_beginning_white_space,
662 inside_white_space: false,
663 following_newline: false,
664 have_seen_non_white_space_characters: false,
665 character_pending_to_return: None,
666 }
667 }
668
669 fn is_leading_trimmed_white_space(&self) -> bool {
670 !self.have_seen_non_white_space_characters && self.remove_collapsible_white_space_at_start
671 }
672
673 fn need_to_produce_space_character_after_white_space(&self) -> bool {
678 self.inside_white_space && !self.following_newline && !self.is_leading_trimmed_white_space()
679 }
680}
681
682impl<InputIterator> Iterator for WhitespaceCollapse<InputIterator>
683where
684 InputIterator: Iterator<Item = char>,
685{
686 type Item = char;
687
688 fn next(&mut self) -> Option<Self::Item> {
689 if self.white_space_collapse == WhiteSpaceCollapse::Preserve ||
695 self.white_space_collapse == WhiteSpaceCollapse::BreakSpaces
696 {
697 return match self.char_iterator.next() {
702 Some('\r') => Some(' '),
703 next => next,
704 };
705 }
706
707 if let Some(character) = self.character_pending_to_return.take() {
708 self.inside_white_space = false;
709 self.have_seen_non_white_space_characters = true;
710 self.following_newline = false;
711 return Some(character);
712 }
713
714 while let Some(character) = self.char_iterator.next() {
715 if InlineFormattingContextBuilder::is_document_white_space(character) &&
719 character != '\n'
720 {
721 self.inside_white_space = true;
722 continue;
723 }
724
725 if character == '\n' {
729 if self.white_space_collapse != WhiteSpaceCollapse::Collapse {
735 self.inside_white_space = false;
736 self.following_newline = true;
737 return Some(character);
738
739 } else if !self.following_newline &&
745 preserve_segment_break() &&
746 !self.is_leading_trimmed_white_space()
747 {
748 self.inside_white_space = false;
749 self.following_newline = true;
750 return Some(' ');
751 } else {
752 self.following_newline = true;
753 continue;
754 }
755 }
756
757 if self.need_to_produce_space_character_after_white_space() {
766 self.inside_white_space = false;
767 self.character_pending_to_return = Some(character);
768 return Some(' ');
769 }
770
771 self.inside_white_space = false;
772 self.have_seen_non_white_space_characters = true;
773 self.following_newline = false;
774 return Some(character);
775 }
776
777 if self.need_to_produce_space_character_after_white_space() {
778 self.inside_white_space = false;
779 return Some(' ');
780 }
781
782 None
783 }
784
785 fn size_hint(&self) -> (usize, Option<usize>) {
786 self.char_iterator.size_hint()
787 }
788
789 fn count(self) -> usize
790 where
791 Self: Sized,
792 {
793 self.char_iterator.count()
794 }
795}
796
797enum PendingCaseConversionResult {
798 Uppercase(ToUppercase),
799 Lowercase(ToLowercase),
800}
801
802impl PendingCaseConversionResult {
803 fn next(&mut self) -> Option<char> {
804 match self {
805 PendingCaseConversionResult::Uppercase(to_uppercase) => to_uppercase.next(),
806 PendingCaseConversionResult::Lowercase(to_lowercase) => to_lowercase.next(),
807 }
808 }
809}
810
811pub struct TextTransformation<InputIterator> {
816 char_iterator: InputIterator,
818 text_transform: TextTransformCase,
820 pending_case_conversion_result: Option<PendingCaseConversionResult>,
823}
824
825impl<InputIterator> TextTransformation<InputIterator> {
826 pub fn new(char_iterator: InputIterator, text_transform: TextTransformCase) -> Self {
827 Self {
828 char_iterator,
829 text_transform,
830 pending_case_conversion_result: None,
831 }
832 }
833}
834
835impl<InputIterator> Iterator for TextTransformation<InputIterator>
836where
837 InputIterator: Iterator<Item = char>,
838{
839 type Item = char;
840
841 fn next(&mut self) -> Option<Self::Item> {
842 if let Some(character) = self
843 .pending_case_conversion_result
844 .as_mut()
845 .and_then(|result| result.next())
846 {
847 return Some(character);
848 }
849 self.pending_case_conversion_result = None;
850
851 for character in self.char_iterator.by_ref() {
852 match self.text_transform {
853 TextTransformCase::None => return Some(character),
854 TextTransformCase::Uppercase => {
855 let mut pending_result =
856 PendingCaseConversionResult::Uppercase(character.to_uppercase());
857 if let Some(character) = pending_result.next() {
858 self.pending_case_conversion_result = Some(pending_result);
859 return Some(character);
860 }
861 },
862 TextTransformCase::Lowercase => {
863 let mut pending_result =
864 PendingCaseConversionResult::Lowercase(character.to_lowercase());
865 if let Some(character) = pending_result.next() {
866 self.pending_case_conversion_result = Some(pending_result);
867 return Some(character);
868 }
869 },
870 TextTransformCase::Capitalize => return Some(character),
873 }
874 }
875 None
876 }
877}
878
879pub struct TextSecurityTransform<InputIterator> {
880 char_iterator: InputIterator,
882 text_security: WebKitTextSecurity,
884}
885
886impl<InputIterator> TextSecurityTransform<InputIterator> {
887 pub fn new(char_iterator: InputIterator, text_security: WebKitTextSecurity) -> Self {
888 Self {
889 char_iterator,
890 text_security,
891 }
892 }
893}
894
895impl<InputIterator> Iterator for TextSecurityTransform<InputIterator>
896where
897 InputIterator: Iterator<Item = char>,
898{
899 type Item = char;
900
901 fn next(&mut self) -> Option<Self::Item> {
902 Some(match self.char_iterator.next()? {
906 '\u{200B}' => '\u{200B}',
910 '\n' => '\n',
912 character => match self.text_security {
913 WebKitTextSecurity::None => character,
914 WebKitTextSecurity::Circle => '○',
915 WebKitTextSecurity::Disc => '●',
916 WebKitTextSecurity::Square => '■',
917 },
918 })
919 }
920}
921
922pub(crate) fn capitalize_string(string: &str, allow_word_at_start: bool) -> String {
925 let mut output_string = String::new();
926 output_string.reserve(string.len());
927
928 let word_segmenter = WordSegmenter::new_auto();
929 let mut bounds = word_segmenter.segment_str(string).peekable();
930 let mut byte_index = 0;
931 for character in string.chars() {
932 let current_byte_index = byte_index;
933 byte_index += character.len_utf8();
934
935 if let Some(next_index) = bounds.peek() &&
936 *next_index == current_byte_index
937 {
938 bounds.next();
939
940 if current_byte_index != 0 || allow_word_at_start {
941 output_string.extend(character.to_uppercase());
942 continue;
943 }
944 }
945
946 output_string.push(character);
947 }
948
949 output_string
950}
951
952fn first_letter_range(text: &str) -> Range<usize> {
962 enum State {
963 Start,
965 PrecedingPunctuation,
967 Lns,
969 TrailingPunctuation,
972 }
973
974 let mut start = 0;
975 let mut state = State::Start;
976 for (index, character) in text.char_indices() {
977 match &mut state {
978 State::Start => {
979 if character.is_letter() || character.is_number() || character.is_symbol() {
980 start = index;
981 state = State::Lns;
982 } else if character.is_punctuation() {
983 start = index;
984 state = State::PrecedingPunctuation
985 }
986 },
987 State::PrecedingPunctuation => {
988 if character.is_letter() || character.is_number() || character.is_symbol() {
989 state = State::Lns;
990 } else if !character.is_separator_space() && !character.is_punctuation() {
991 return 0..0;
992 }
993 },
994 State::Lns => {
995 if character.is_punctuation() &&
998 !character.is_punctuation_open() &&
999 !character.is_punctuation_dash()
1000 {
1001 state = State::TrailingPunctuation;
1002 } else {
1003 return start..index;
1004 }
1005 },
1006 State::TrailingPunctuation => {
1007 if character.is_punctuation() &&
1010 !character.is_punctuation_open() &&
1011 !character.is_punctuation_dash()
1012 {
1013 continue;
1014 } else {
1015 return start..index;
1016 }
1017 },
1018 }
1019 }
1020
1021 match state {
1022 State::Start | State::PrecedingPunctuation => 0..0,
1023 State::Lns | State::TrailingPunctuation => start..text.len(),
1024 }
1025}
1026
1027#[cfg(test)]
1028mod tests {
1029 use super::*;
1030
1031 fn assert_first_letter_eq(text: &str, expected: &str) {
1032 let range = first_letter_range(text);
1033 assert_eq!(&text[range], expected);
1034 }
1035
1036 #[test]
1037 fn test_first_letter_range() {
1038 assert_first_letter_eq("", "");
1040 assert_first_letter_eq(" ", "");
1041
1042 assert_first_letter_eq("(", "");
1044 assert_first_letter_eq(" (", "");
1045 assert_first_letter_eq("( ", "");
1046 assert_first_letter_eq("()", "");
1047
1048 assert_first_letter_eq("\u{0903}", "");
1050
1051 assert_first_letter_eq("A", "A");
1053 assert_first_letter_eq(" A", "A");
1054 assert_first_letter_eq("A ", "A");
1055 assert_first_letter_eq(" A ", "A");
1056
1057 assert_first_letter_eq("App", "A");
1059 assert_first_letter_eq(" App", "A");
1060 assert_first_letter_eq("App ", "A");
1061
1062 assert_first_letter_eq(r#""A"#, r#""A"#);
1064 assert_first_letter_eq(r#" "A"#, r#""A"#);
1065 assert_first_letter_eq(r#""A "#, r#""A"#);
1066 assert_first_letter_eq(r#"" A"#, r#"" A"#);
1067 assert_first_letter_eq(r#" "A "#, r#""A"#);
1068 assert_first_letter_eq(r#"("A"#, r#"("A"#);
1069 assert_first_letter_eq(r#" ("A"#, r#"("A"#);
1070 assert_first_letter_eq(r#"( "A"#, r#"( "A"#);
1071 assert_first_letter_eq(r#"[ ( "A"#, r#"[ ( "A"#);
1072
1073 assert_first_letter_eq(r#"A""#, r#"A""#);
1076 assert_first_letter_eq(r#"A" "#, r#"A""#);
1077 assert_first_letter_eq(r#"A)]"#, r#"A)]"#);
1078 assert_first_letter_eq(r#"A" )]"#, r#"A""#);
1079 assert_first_letter_eq(r#"A)] >"#, r#"A)]"#);
1080
1081 assert_first_letter_eq(r#" ("A" )]"#, r#"("A""#);
1083 assert_first_letter_eq(r#" ("A")] >"#, r#"("A")]"#);
1084
1085 assert_first_letter_eq("一", "一");
1087 assert_first_letter_eq(" 一 ", "一");
1088 assert_first_letter_eq("一二三", "一");
1089 assert_first_letter_eq(" 一二三 ", "一");
1090 assert_first_letter_eq("(一二三)", "(一");
1091 assert_first_letter_eq(" (一二三) ", "(一");
1092 assert_first_letter_eq("((一", "((一");
1093 assert_first_letter_eq(" ( (一", "( (一");
1094 assert_first_letter_eq("一)", "一)");
1095 assert_first_letter_eq("一))", "一))");
1096 assert_first_letter_eq("一) )", "一)");
1097 }
1098}