mirror of
https://github.com/servo/servo.git
synced 2025-06-04 07:35:36 +00:00
* Use 2024 style edition Signed-off-by: Simon Wülker <simon.wuelker@arcor.de> * Reformat all code Signed-off-by: Simon Wülker <simon.wuelker@arcor.de> --------- Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
625 lines
25 KiB
Rust
625 lines
25 KiB
Rust
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
|
|
use std::borrow::Cow;
|
|
use std::char::{ToLowercase, ToUppercase};
|
|
|
|
use icu_segmenter::WordSegmenter;
|
|
use servo_arc::Arc;
|
|
use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
|
|
use style::values::specified::text::TextTransformCase;
|
|
use unicode_bidi::Level;
|
|
|
|
use super::text_run::TextRun;
|
|
use super::{InlineBox, InlineBoxIdentifier, InlineBoxes, InlineFormattingContext, InlineItem};
|
|
use crate::PropagatedBoxTreeData;
|
|
use crate::cell::ArcRefCell;
|
|
use crate::context::LayoutContext;
|
|
use crate::dom::NodeExt;
|
|
use crate::dom_traversal::NodeAndStyleInfo;
|
|
use crate::flow::float::FloatBox;
|
|
use crate::formatting_contexts::IndependentFormattingContext;
|
|
use crate::positioned::AbsolutelyPositionedBox;
|
|
use crate::style_ext::ComputedValuesExt;
|
|
|
|
#[derive(Default)]
|
|
pub(crate) struct InlineFormattingContextBuilder {
|
|
/// The collection of text strings that make up this [`InlineFormattingContext`] under
|
|
/// construction.
|
|
pub text_segments: Vec<String>,
|
|
|
|
/// The current offset in the final text string of this [`InlineFormattingContext`],
|
|
/// used to properly set the text range of new [`InlineItem::TextRun`]s.
|
|
current_text_offset: usize,
|
|
|
|
/// Whether the last processed node ended with whitespace. This is used to
|
|
/// implement rule 4 of <https://www.w3.org/TR/css-text-3/#collapse>:
|
|
///
|
|
/// > Any collapsible space immediately following another collapsible space—even one
|
|
/// > outside the boundary of the inline containing that space, provided both spaces are
|
|
/// > within the same inline formatting context—is collapsed to have zero advance width.
|
|
/// > (It is invisible, but retains its soft wrap opportunity, if any.)
|
|
last_inline_box_ended_with_collapsible_white_space: bool,
|
|
|
|
/// Whether or not the current state of the inline formatting context is on a word boundary
|
|
/// for the purposes of `text-transform: capitalize`.
|
|
on_word_boundary: bool,
|
|
|
|
/// Whether or not this inline formatting context will contain floats.
|
|
pub contains_floats: bool,
|
|
|
|
/// The current list of [`InlineItem`]s in this [`InlineFormattingContext`] under
|
|
/// construction. This is stored in a flat list to make it easy to access the last
|
|
/// item.
|
|
pub inline_items: Vec<ArcRefCell<InlineItem>>,
|
|
|
|
/// The current [`InlineBox`] tree of this [`InlineFormattingContext`] under construction.
|
|
pub inline_boxes: InlineBoxes,
|
|
|
|
/// The ongoing stack of inline boxes stack of the builder.
|
|
///
|
|
/// Contains all the currently ongoing inline boxes we entered so far.
|
|
/// The traversal is at all times as deep in the tree as this stack is,
|
|
/// which is why the code doesn't need to keep track of the actual
|
|
/// container root (see `handle_inline_level_element`).
|
|
///
|
|
/// When an inline box ends, it's removed from this stack.
|
|
inline_box_stack: Vec<InlineBoxIdentifier>,
|
|
|
|
/// Whether or not the inline formatting context under construction has any
|
|
/// uncollapsible text content.
|
|
pub has_uncollapsible_text_content: bool,
|
|
}
|
|
|
|
impl InlineFormattingContextBuilder {
|
|
pub(crate) fn new() -> Self {
|
|
// For the purposes of `text-transform: capitalize` the start of the IFC is a word boundary.
|
|
Self {
|
|
on_word_boundary: true,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
pub(crate) fn currently_processing_inline_box(&self) -> bool {
|
|
!self.inline_box_stack.is_empty()
|
|
}
|
|
|
|
fn push_control_character_string(&mut self, string_to_push: &str) {
|
|
self.text_segments.push(string_to_push.to_owned());
|
|
self.current_text_offset += string_to_push.len();
|
|
}
|
|
|
|
/// Return true if this [`InlineFormattingContextBuilder`] is empty for the purposes of ignoring
|
|
/// during box tree construction. An IFC is empty if it only contains TextRuns with
|
|
/// completely collapsible whitespace. When that happens it can be ignored completely.
|
|
pub(crate) fn is_empty(&self) -> bool {
|
|
if self.has_uncollapsible_text_content {
|
|
return false;
|
|
}
|
|
|
|
if !self.inline_box_stack.is_empty() {
|
|
return false;
|
|
}
|
|
|
|
fn inline_level_box_is_empty(inline_level_box: &InlineItem) -> bool {
|
|
match inline_level_box {
|
|
InlineItem::StartInlineBox(_) => false,
|
|
InlineItem::EndInlineBox => false,
|
|
// Text content is handled by `self.has_uncollapsible_text` content above in order
|
|
// to avoid having to iterate through the character once again.
|
|
InlineItem::TextRun(_) => true,
|
|
InlineItem::OutOfFlowAbsolutelyPositionedBox(..) => false,
|
|
InlineItem::OutOfFlowFloatBox(_) => false,
|
|
InlineItem::Atomic(..) => false,
|
|
}
|
|
}
|
|
|
|
self.inline_items
|
|
.iter()
|
|
.all(|inline_level_box| inline_level_box_is_empty(&inline_level_box.borrow()))
|
|
}
|
|
|
|
pub(crate) fn push_atomic(
|
|
&mut self,
|
|
independent_formatting_context: IndependentFormattingContext,
|
|
) -> ArcRefCell<InlineItem> {
|
|
let inline_level_box = ArcRefCell::new(InlineItem::Atomic(
|
|
Arc::new(independent_formatting_context),
|
|
self.current_text_offset,
|
|
Level::ltr(), /* This will be assigned later if necessary. */
|
|
));
|
|
self.inline_items.push(inline_level_box.clone());
|
|
|
|
// Push an object replacement character for this atomic, which will ensure that the line breaker
|
|
// inserts a line breaking opportunity here.
|
|
self.push_control_character_string("\u{fffc}");
|
|
|
|
self.last_inline_box_ended_with_collapsible_white_space = false;
|
|
self.on_word_boundary = true;
|
|
|
|
inline_level_box
|
|
}
|
|
|
|
pub(crate) fn push_absolutely_positioned_box(
|
|
&mut self,
|
|
absolutely_positioned_box: AbsolutelyPositionedBox,
|
|
) -> ArcRefCell<InlineItem> {
|
|
let absolutely_positioned_box = ArcRefCell::new(absolutely_positioned_box);
|
|
let inline_level_box = ArcRefCell::new(InlineItem::OutOfFlowAbsolutelyPositionedBox(
|
|
absolutely_positioned_box,
|
|
self.current_text_offset,
|
|
));
|
|
|
|
self.inline_items.push(inline_level_box.clone());
|
|
inline_level_box
|
|
}
|
|
|
|
pub(crate) fn push_float_box(&mut self, float_box: FloatBox) -> ArcRefCell<InlineItem> {
|
|
let inline_level_box = ArcRefCell::new(InlineItem::OutOfFlowFloatBox(Arc::new(float_box)));
|
|
self.inline_items.push(inline_level_box.clone());
|
|
self.contains_floats = true;
|
|
inline_level_box
|
|
}
|
|
|
|
pub(crate) fn start_inline_box(&mut self, inline_box: InlineBox) -> ArcRefCell<InlineItem> {
|
|
self.push_control_character_string(inline_box.style.bidi_control_chars().0);
|
|
|
|
let (identifier, inline_box) = self.inline_boxes.start_inline_box(inline_box);
|
|
let inline_level_box = ArcRefCell::new(InlineItem::StartInlineBox(inline_box));
|
|
self.inline_items.push(inline_level_box.clone());
|
|
self.inline_box_stack.push(identifier);
|
|
inline_level_box
|
|
}
|
|
|
|
pub(crate) fn end_inline_box(&mut self) -> ArcRefCell<InlineBox> {
|
|
let identifier = self.end_inline_box_internal();
|
|
let inline_level_box = self.inline_boxes.get(&identifier);
|
|
inline_level_box.borrow_mut().is_last_fragment = true;
|
|
|
|
self.push_control_character_string(inline_level_box.borrow().style.bidi_control_chars().1);
|
|
|
|
inline_level_box
|
|
}
|
|
|
|
fn end_inline_box_internal(&mut self) -> InlineBoxIdentifier {
|
|
let identifier = self
|
|
.inline_box_stack
|
|
.pop()
|
|
.expect("Ended non-existent inline box");
|
|
self.inline_items
|
|
.push(ArcRefCell::new(InlineItem::EndInlineBox));
|
|
|
|
self.inline_boxes.end_inline_box(identifier);
|
|
identifier
|
|
}
|
|
|
|
pub(crate) fn push_text<'dom, Node: NodeExt<'dom>>(
|
|
&mut self,
|
|
text: Cow<'dom, str>,
|
|
info: &NodeAndStyleInfo<Node>,
|
|
) {
|
|
let white_space_collapse = info.style.clone_white_space_collapse();
|
|
let collapsed = WhitespaceCollapse::new(
|
|
text.chars(),
|
|
white_space_collapse,
|
|
self.last_inline_box_ended_with_collapsible_white_space,
|
|
);
|
|
|
|
// TODO: Not all text transforms are about case, this logic should stop ignoring
|
|
// TextTransform::FULL_WIDTH and TextTransform::FULL_SIZE_KANA.
|
|
let text_transform = info.style.clone_text_transform().case();
|
|
let capitalized_text: String;
|
|
let char_iterator: Box<dyn Iterator<Item = char>> = match text_transform {
|
|
TextTransformCase::None => Box::new(collapsed),
|
|
TextTransformCase::Capitalize => {
|
|
// `TextTransformation` doesn't support capitalization, so we must capitalize the whole
|
|
// string at once and make a copy. Here `on_word_boundary` indicates whether or not the
|
|
// inline formatting context as a whole is on a word boundary. This is different from
|
|
// `last_inline_box_ended_with_collapsible_white_space` because the word boundaries are
|
|
// between atomic inlines and at the start of the IFC, and because preserved spaces
|
|
// are a word boundary.
|
|
let collapsed_string: String = collapsed.collect();
|
|
capitalized_text = capitalize_string(&collapsed_string, self.on_word_boundary);
|
|
Box::new(capitalized_text.chars())
|
|
},
|
|
_ => {
|
|
// If `text-transform` is active, wrap the `WhitespaceCollapse` iterator in
|
|
// a `TextTransformation` iterator.
|
|
Box::new(TextTransformation::new(collapsed, text_transform))
|
|
},
|
|
};
|
|
|
|
let white_space_collapse = info.style.clone_white_space_collapse();
|
|
let new_text: String = char_iterator
|
|
.inspect(|&character| {
|
|
self.has_uncollapsible_text_content |= matches!(
|
|
white_space_collapse,
|
|
WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces
|
|
) || !character.is_ascii_whitespace() ||
|
|
(character == '\n' && white_space_collapse != WhiteSpaceCollapse::Collapse);
|
|
})
|
|
.collect();
|
|
|
|
if new_text.is_empty() {
|
|
return;
|
|
}
|
|
|
|
if let Some(last_character) = new_text.chars().next_back() {
|
|
self.on_word_boundary = last_character.is_whitespace();
|
|
self.last_inline_box_ended_with_collapsible_white_space =
|
|
self.on_word_boundary && white_space_collapse != WhiteSpaceCollapse::Preserve;
|
|
}
|
|
|
|
let new_range = self.current_text_offset..self.current_text_offset + new_text.len();
|
|
self.current_text_offset = new_range.end;
|
|
self.text_segments.push(new_text);
|
|
|
|
if let Some(inline_item) = self.inline_items.last() {
|
|
if let InlineItem::TextRun(text_run) = &mut *inline_item.borrow_mut() {
|
|
text_run.borrow_mut().text_range.end = new_range.end;
|
|
return;
|
|
}
|
|
}
|
|
|
|
self.inline_items
|
|
.push(ArcRefCell::new(InlineItem::TextRun(ArcRefCell::new(
|
|
TextRun::new(info.into(), info.style.clone(), new_range),
|
|
))));
|
|
}
|
|
|
|
pub(crate) fn split_around_block_and_finish(
|
|
&mut self,
|
|
layout_context: &LayoutContext,
|
|
propagated_data: PropagatedBoxTreeData,
|
|
has_first_formatted_line: bool,
|
|
default_bidi_level: Level,
|
|
) -> Option<InlineFormattingContext> {
|
|
if self.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
// Create a new inline builder which will be active after the block splits this inline formatting
|
|
// context. It has the same inline box structure as this builder, except the boxes are
|
|
// marked as not being the first fragment. No inline content is carried over to this new
|
|
// builder.
|
|
let mut new_builder = InlineFormattingContextBuilder::new();
|
|
for identifier in self.inline_box_stack.iter() {
|
|
new_builder.start_inline_box(
|
|
self.inline_boxes
|
|
.get(identifier)
|
|
.borrow()
|
|
.split_around_block(),
|
|
);
|
|
}
|
|
let mut inline_builder_from_before_split = std::mem::replace(self, new_builder);
|
|
|
|
// End all ongoing inline boxes in the first builder, but ensure that they are not
|
|
// marked as the final fragments, so that they do not get inline end margin, borders,
|
|
// and padding.
|
|
while !inline_builder_from_before_split.inline_box_stack.is_empty() {
|
|
inline_builder_from_before_split.end_inline_box_internal();
|
|
}
|
|
|
|
inline_builder_from_before_split.finish(
|
|
layout_context,
|
|
propagated_data,
|
|
has_first_formatted_line,
|
|
/* is_single_line_text_input = */ false,
|
|
default_bidi_level,
|
|
)
|
|
}
|
|
|
|
/// Finish the current inline formatting context, returning [`None`] if the context was empty.
|
|
pub(crate) fn finish(
|
|
&mut self,
|
|
layout_context: &LayoutContext,
|
|
propagated_data: PropagatedBoxTreeData,
|
|
has_first_formatted_line: bool,
|
|
is_single_line_text_input: bool,
|
|
default_bidi_level: Level,
|
|
) -> Option<InlineFormattingContext> {
|
|
if self.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
let old_builder = std::mem::replace(self, InlineFormattingContextBuilder::new());
|
|
assert!(old_builder.inline_box_stack.is_empty());
|
|
|
|
Some(InlineFormattingContext::new_with_builder(
|
|
old_builder,
|
|
layout_context,
|
|
propagated_data,
|
|
has_first_formatted_line,
|
|
is_single_line_text_input,
|
|
default_bidi_level,
|
|
))
|
|
}
|
|
}
|
|
|
|
fn preserve_segment_break() -> bool {
|
|
true
|
|
}
|
|
|
|
pub struct WhitespaceCollapse<InputIterator> {
|
|
char_iterator: InputIterator,
|
|
white_space_collapse: WhiteSpaceCollapse,
|
|
|
|
/// Whether or not we should collapse white space completely at the start of the string.
|
|
/// This is true when the last character handled in our owning [`super::InlineFormattingContext`]
|
|
/// was collapsible white space.
|
|
remove_collapsible_white_space_at_start: bool,
|
|
|
|
/// Whether or not the last character produced was newline. There is special behavior
|
|
/// we do after each newline.
|
|
following_newline: bool,
|
|
|
|
/// Whether or not we have seen any non-white space characters, indicating that we are not
|
|
/// in a collapsible white space section at the beginning of the string.
|
|
have_seen_non_white_space_characters: bool,
|
|
|
|
/// Whether the last character that we processed was a non-newline white space character. When
|
|
/// collapsing white space we need to wait until the next non-white space character or the end
|
|
/// of the string to push a single white space.
|
|
inside_white_space: bool,
|
|
|
|
/// When we enter a collapsible white space region, we may need to wait to produce a single
|
|
/// white space character as soon as we encounter a non-white space character. When that
|
|
/// happens we queue up the non-white space character for the next iterator call.
|
|
character_pending_to_return: Option<char>,
|
|
}
|
|
|
|
impl<InputIterator> WhitespaceCollapse<InputIterator> {
|
|
pub fn new(
|
|
char_iterator: InputIterator,
|
|
white_space_collapse: WhiteSpaceCollapse,
|
|
trim_beginning_white_space: bool,
|
|
) -> Self {
|
|
Self {
|
|
char_iterator,
|
|
white_space_collapse,
|
|
remove_collapsible_white_space_at_start: trim_beginning_white_space,
|
|
inside_white_space: false,
|
|
following_newline: false,
|
|
have_seen_non_white_space_characters: false,
|
|
character_pending_to_return: None,
|
|
}
|
|
}
|
|
|
|
fn is_leading_trimmed_white_space(&self) -> bool {
|
|
!self.have_seen_non_white_space_characters && self.remove_collapsible_white_space_at_start
|
|
}
|
|
|
|
/// Whether or not we need to produce a space character if the next character is not a newline
|
|
/// and not white space. This happens when we are exiting a section of white space and we
|
|
/// waited to produce a single space character for the entire section of white space (but
|
|
/// not following or preceding a newline).
|
|
fn need_to_produce_space_character_after_white_space(&self) -> bool {
|
|
self.inside_white_space && !self.following_newline && !self.is_leading_trimmed_white_space()
|
|
}
|
|
}
|
|
|
|
impl<InputIterator> Iterator for WhitespaceCollapse<InputIterator>
|
|
where
|
|
InputIterator: Iterator<Item = char>,
|
|
{
|
|
type Item = char;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
// Point 4.1.1 first bullet:
|
|
// > If white-space is set to normal, nowrap, or pre-line, whitespace
|
|
// > characters are considered collapsible
|
|
// If whitespace is not considered collapsible, it is preserved entirely, which
|
|
// means that we can simply return the input string exactly.
|
|
if self.white_space_collapse == WhiteSpaceCollapse::Preserve ||
|
|
self.white_space_collapse == WhiteSpaceCollapse::BreakSpaces
|
|
{
|
|
// From <https://drafts.csswg.org/css-text-3/#white-space-processing>:
|
|
// > Carriage returns (U+000D) are treated identically to spaces (U+0020) in all respects.
|
|
//
|
|
// In the non-preserved case these are converted to space below.
|
|
return match self.char_iterator.next() {
|
|
Some('\r') => Some(' '),
|
|
next => next,
|
|
};
|
|
}
|
|
|
|
if let Some(character) = self.character_pending_to_return.take() {
|
|
self.inside_white_space = false;
|
|
self.have_seen_non_white_space_characters = true;
|
|
self.following_newline = false;
|
|
return Some(character);
|
|
}
|
|
|
|
while let Some(character) = self.char_iterator.next() {
|
|
// Don't push non-newline whitespace immediately. Instead wait to push it until we
|
|
// know that it isn't followed by a newline. See `push_pending_whitespace_if_needed`
|
|
// above.
|
|
if character.is_ascii_whitespace() && character != '\n' {
|
|
self.inside_white_space = true;
|
|
continue;
|
|
}
|
|
|
|
// Point 4.1.1:
|
|
// > 2. Collapsible segment breaks are transformed for rendering according to the
|
|
// > segment break transformation rules.
|
|
if character == '\n' {
|
|
// From <https://drafts.csswg.org/css-text-3/#line-break-transform>
|
|
// (4.1.3 -- the segment break transformation rules):
|
|
//
|
|
// > When white-space is pre, pre-wrap, or pre-line, segment breaks are not
|
|
// > collapsible and are instead transformed into a preserved line feed"
|
|
if self.white_space_collapse != WhiteSpaceCollapse::Collapse {
|
|
self.inside_white_space = false;
|
|
self.following_newline = true;
|
|
return Some(character);
|
|
|
|
// Point 4.1.3:
|
|
// > 1. First, any collapsible segment break immediately following another
|
|
// > collapsible segment break is removed.
|
|
// > 2. Then any remaining segment break is either transformed into a space (U+0020)
|
|
// > or removed depending on the context before and after the break.
|
|
} else if !self.following_newline &&
|
|
preserve_segment_break() &&
|
|
!self.is_leading_trimmed_white_space()
|
|
{
|
|
self.inside_white_space = false;
|
|
self.following_newline = true;
|
|
return Some(' ');
|
|
} else {
|
|
self.following_newline = true;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Point 4.1.1:
|
|
// > 2. Any sequence of collapsible spaces and tabs immediately preceding or
|
|
// > following a segment break is removed.
|
|
// > 3. Every collapsible tab is converted to a collapsible space (U+0020).
|
|
// > 4. Any collapsible space immediately following another collapsible space—even
|
|
// > one outside the boundary of the inline containing that space, provided both
|
|
// > spaces are within the same inline formatting context—is collapsed to have zero
|
|
// > advance width.
|
|
if self.need_to_produce_space_character_after_white_space() {
|
|
self.inside_white_space = false;
|
|
self.character_pending_to_return = Some(character);
|
|
return Some(' ');
|
|
}
|
|
|
|
self.inside_white_space = false;
|
|
self.have_seen_non_white_space_characters = true;
|
|
self.following_newline = false;
|
|
return Some(character);
|
|
}
|
|
|
|
if self.need_to_produce_space_character_after_white_space() {
|
|
self.inside_white_space = false;
|
|
return Some(' ');
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
|
self.char_iterator.size_hint()
|
|
}
|
|
|
|
fn count(self) -> usize
|
|
where
|
|
Self: Sized,
|
|
{
|
|
self.char_iterator.count()
|
|
}
|
|
}
|
|
|
|
enum PendingCaseConversionResult {
|
|
Uppercase(ToUppercase),
|
|
Lowercase(ToLowercase),
|
|
}
|
|
|
|
impl PendingCaseConversionResult {
|
|
fn next(&mut self) -> Option<char> {
|
|
match self {
|
|
PendingCaseConversionResult::Uppercase(to_uppercase) => to_uppercase.next(),
|
|
PendingCaseConversionResult::Lowercase(to_lowercase) => to_lowercase.next(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// This is an interator that consumes a char iterator and produces character transformed
|
|
/// by the given CSS `text-transform` value. It currently does not support
|
|
/// `text-transform: capitalize` because Unicode segmentation libraries do not support
|
|
/// streaming input one character at a time.
|
|
pub struct TextTransformation<InputIterator> {
|
|
/// The input character iterator.
|
|
char_iterator: InputIterator,
|
|
/// The `text-transform` value to use.
|
|
text_transform: TextTransformCase,
|
|
/// If an uppercasing or lowercasing produces more than one character, this
|
|
/// caches them so that they can be returned in subsequent iterator calls.
|
|
pending_case_conversion_result: Option<PendingCaseConversionResult>,
|
|
}
|
|
|
|
impl<InputIterator> TextTransformation<InputIterator> {
|
|
pub fn new(char_iterator: InputIterator, text_transform: TextTransformCase) -> Self {
|
|
Self {
|
|
char_iterator,
|
|
text_transform,
|
|
pending_case_conversion_result: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<InputIterator> Iterator for TextTransformation<InputIterator>
|
|
where
|
|
InputIterator: Iterator<Item = char>,
|
|
{
|
|
type Item = char;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
if let Some(character) = self
|
|
.pending_case_conversion_result
|
|
.as_mut()
|
|
.and_then(|result| result.next())
|
|
{
|
|
return Some(character);
|
|
}
|
|
self.pending_case_conversion_result = None;
|
|
|
|
for character in self.char_iterator.by_ref() {
|
|
match self.text_transform {
|
|
TextTransformCase::None => return Some(character),
|
|
TextTransformCase::Uppercase => {
|
|
let mut pending_result =
|
|
PendingCaseConversionResult::Uppercase(character.to_uppercase());
|
|
if let Some(character) = pending_result.next() {
|
|
self.pending_case_conversion_result = Some(pending_result);
|
|
return Some(character);
|
|
}
|
|
},
|
|
TextTransformCase::Lowercase => {
|
|
let mut pending_result =
|
|
PendingCaseConversionResult::Lowercase(character.to_lowercase());
|
|
if let Some(character) = pending_result.next() {
|
|
self.pending_case_conversion_result = Some(pending_result);
|
|
return Some(character);
|
|
}
|
|
},
|
|
// `text-transform: capitalize` currently cannot work on a per-character basis,
|
|
// so must be handled outside of this iterator.
|
|
TextTransformCase::Capitalize => return Some(character),
|
|
}
|
|
}
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Given a string and whether the start of the string represents a word boundary, create a copy of
|
|
/// the string with letters after word boundaries capitalized.
|
|
fn capitalize_string(string: &str, allow_word_at_start: bool) -> String {
|
|
let mut output_string = String::new();
|
|
output_string.reserve(string.len());
|
|
|
|
let word_segmenter = WordSegmenter::new_auto();
|
|
let mut bounds = word_segmenter.segment_str(string).peekable();
|
|
let mut byte_index = 0;
|
|
for character in string.chars() {
|
|
let current_byte_index = byte_index;
|
|
byte_index += character.len_utf8();
|
|
|
|
if let Some(next_index) = bounds.peek() {
|
|
if *next_index == current_byte_index {
|
|
bounds.next();
|
|
|
|
if current_byte_index != 0 || allow_word_at_start {
|
|
output_string.extend(character.to_uppercase());
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
output_string.push(character);
|
|
}
|
|
|
|
output_string
|
|
}
|