diff --git a/components/layout_2020/flexbox/construct.rs b/components/layout_2020/flexbox/construct.rs index 55e94b35655..b5aa9b34c47 100644 --- a/components/layout_2020/flexbox/construct.rs +++ b/components/layout_2020/flexbox/construct.rs @@ -153,7 +153,6 @@ where (&run.info).into(), run.info.style, run.text.into(), - false, /* has_uncollapsible_content */ ) }); let bfc = BlockFormattingContext::construct_for_text_runs( diff --git a/components/layout_2020/flow/construct.rs b/components/layout_2020/flow/construct.rs index fc49b8beef7..1a512910374 100644 --- a/components/layout_2020/flow/construct.rs +++ b/components/layout_2020/flow/construct.rs @@ -7,7 +7,6 @@ use std::convert::{TryFrom, TryInto}; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use servo_arc::Arc; -use style::computed_values::white_space::T as WhiteSpace; use style::properties::longhands::list_style_position::computed_value::T as ListStylePosition; use style::properties::ComputedValues; use style::selector_parser::PseudoElement; @@ -60,7 +59,6 @@ impl BlockFormattingContext { layout_context: &LayoutContext, text_decoration_line: TextDecorationLine, ) -> Self { - // FIXME: do white space collapsing let inline_level_boxes = runs .map(|run| ArcRefCell::new(InlineLevelBox::TextRun(run))) .collect(); @@ -71,7 +69,6 @@ impl BlockFormattingContext { text_decoration_line, has_first_formatted_line: true, contains_floats: false, - ends_with_whitespace: false, }; Self { contents: BlockContainer::construct_inline_formatting_context(layout_context, ifc), @@ -114,7 +111,7 @@ enum BlockLevelCreator { /// /// Deferring allows using rayon’s `into_par_iter`. enum IntermediateBlockContainer { - InlineFormattingContext(InlineFormattingContext), + InlineFormattingContext(BlockContainer), Deferred { contents: NonReplacedContents, propagated_text_decoration_line: TextDecorationLine, @@ -247,7 +244,6 @@ where ongoing_inline_formatting_context: InlineFormattingContext::new( text_decoration_line, /* has_first_formatted_line = */ true, - /* ends_with_whitespace */ false, ), ongoing_inline_boxes_stack: Vec::new(), anonymous_style: None, @@ -318,7 +314,6 @@ where ); if inline_table { - self.ongoing_inline_formatting_context.ends_with_whitespace = false; self.current_inline_level_boxes() .push(ArcRefCell::new(InlineLevelBox::Atomic(ifc))); } else { @@ -409,24 +404,15 @@ where self.finish_anonymous_table_if_needed(); } - let (output, has_uncollapsible_content) = collapse_and_transform_whitespace( - &input, - info.style.get_inherited_text().white_space, - self.ongoing_inline_formatting_context.ends_with_whitespace, - ); - if output.is_empty() { - return; - } - - self.ongoing_inline_formatting_context.ends_with_whitespace = - output.chars().last().unwrap().is_ascii_whitespace(); - + // TODO: We can do better here than `push_str` and wait until we are breaking and + // shaping text to allocate space big enough for the final text. It would require + // collecting all Cow strings into a vector and passing them along to text breaking + // and shaping during final InlineFormattingContext construction. let inlines = self.current_inline_level_boxes(); match inlines.last_mut().map(|last| last.borrow_mut()) { Some(mut last_box) => match *last_box { InlineLevelBox::TextRun(ref mut text_run) => { - text_run.text.push_str(&output); - text_run.has_uncollapsible_content |= has_uncollapsible_content; + text_run.text.push_str(&input); return; }, _ => {}, @@ -437,130 +423,11 @@ where inlines.push(ArcRefCell::new(InlineLevelBox::TextRun(TextRun::new( info.into(), Arc::clone(&info.style), - output, - has_uncollapsible_content, + input.into(), )))); } } -fn preserve_segment_break() -> bool { - true -} - -/// Collapse and transform whitespace in the given input according to the rules in -/// . This method doesn't -/// follow the steps exactly since they are defined in a multi-pass appraoach, but it -/// tries to be effectively the same transformation. -/// -/// Returns the transformed text as a [String] and also whether or not the input had -/// any uncollapsible content. -fn collapse_and_transform_whitespace( - input: &str, - white_space: WhiteSpace, - trim_beginning_white_space: bool, -) -> (String, bool) { - // 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 white_space.preserve_spaces() { - return (input.to_owned(), true); - } - - let mut output = String::with_capacity(input.len()); - let mut has_uncollapsible_content = false; - let mut had_whitespace = false; - let mut following_newline = false; - let mut in_whitespace_at_beginning = true; - - let is_leading_trimmed_whitespace = - |in_whitespace_at_beginning: bool| in_whitespace_at_beginning && trim_beginning_white_space; - - // 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. - let push_pending_whitespace_if_needed = - |output: &mut String, - had_whitespace: bool, - following_newline: bool, - in_whitespace_at_beginning: bool| { - if had_whitespace && - !following_newline && - !is_leading_trimmed_whitespace(in_whitespace_at_beginning) - { - output.push(' '); - } - }; - - for character in input.chars() { - // 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' { - had_whitespace = 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 - // (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 white_space == WhiteSpace::PreLine { - has_uncollapsible_content = true; - had_whitespace = false; - output.push('\n'); - - // 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 !following_newline && - preserve_segment_break() && - !is_leading_trimmed_whitespace(in_whitespace_at_beginning) - { - had_whitespace = false; - output.push(' '); - } - following_newline = true; - continue; - } - - push_pending_whitespace_if_needed( - &mut output, - had_whitespace, - following_newline, - in_whitespace_at_beginning, - ); - - has_uncollapsible_content = true; - had_whitespace = false; - in_whitespace_at_beginning = false; - following_newline = false; - output.push(character); - } - - push_pending_whitespace_if_needed( - &mut output, - had_whitespace, - following_newline, - in_whitespace_at_beginning, - ); - - (output, has_uncollapsible_content) -} - impl<'dom, Node> BlockContainerBuilder<'dom, '_, Node> where Node: NodeExt<'dom>, @@ -630,7 +497,6 @@ where inline_box.is_last_fragment = true; ArcRefCell::new(InlineLevelBox::InlineBox(inline_box)) } else { - self.ongoing_inline_formatting_context.ends_with_whitespace = false; ArcRefCell::new(InlineLevelBox::Atomic( IndependentFormattingContext::construct( self.context, @@ -802,7 +668,6 @@ where // There should never be an empty inline formatting context. self.ongoing_inline_formatting_context .has_first_formatted_line = false; - self.ongoing_inline_formatting_context.ends_with_whitespace = false; return; } @@ -822,7 +687,6 @@ where let mut ifc = InlineFormattingContext::new( self.ongoing_inline_formatting_context.text_decoration_line, /* has_first_formatted_line = */ false, - /* ends_with_whitespace */ false, ); std::mem::swap(&mut self.ongoing_inline_formatting_context, &mut ifc); @@ -832,7 +696,9 @@ where // FIXME(nox): We should be storing this somewhere. box_slot: BoxSlot::dummy(), kind: BlockLevelCreator::SameFormattingContextBlock( - IntermediateBlockContainer::InlineFormattingContext(ifc), + IntermediateBlockContainer::InlineFormattingContext( + BlockContainer::construct_inline_formatting_context(self.context, ifc), + ), ), }); } @@ -932,56 +798,7 @@ impl IntermediateBlockContainer { propagated_text_decoration_line, is_list_item, ), - IntermediateBlockContainer::InlineFormattingContext(ifc) => { - BlockContainer::construct_inline_formatting_context(context, ifc) - }, + IntermediateBlockContainer::InlineFormattingContext(block_container) => block_container, } } } - -#[test] -fn test_collapase_and_transform_whitespace() { - let output = collapse_and_transform_whitespace("H ", WhiteSpace::Normal, false); - assert_eq!(output.0, "H "); - assert!(output.1); - - let output = collapse_and_transform_whitespace(" W", WhiteSpace::Normal, true); - assert_eq!(output.0, "W"); - assert!(output.1); - - let output = collapse_and_transform_whitespace(" W", WhiteSpace::Normal, false); - assert_eq!(output.0, " W"); - assert!(output.1); - - let output = collapse_and_transform_whitespace(" H W", WhiteSpace::Normal, false); - assert_eq!(output.0, " H W"); - assert!(output.1); - - let output = collapse_and_transform_whitespace("\n H \n \t W", WhiteSpace::Normal, false); - assert_eq!(output.0, " H W"); - - let output = collapse_and_transform_whitespace("\n H \n \t W \n", WhiteSpace::Pre, false); - assert_eq!(output.0, "\n H \n \t W \n"); - assert!(output.1); - - let output = - collapse_and_transform_whitespace("\n H \n \t W \n ", WhiteSpace::PreLine, false); - assert_eq!(output.0, "\nH\nW\n"); - assert!(output.1); - - let output = collapse_and_transform_whitespace(" ", WhiteSpace::Normal, true); - assert_eq!(output.0, ""); - assert!(!output.1); - - let output = collapse_and_transform_whitespace(" ", WhiteSpace::Normal, false); - assert_eq!(output.0, " "); - assert!(!output.1); - - let output = collapse_and_transform_whitespace("\n ", WhiteSpace::Normal, true); - assert_eq!(output.0, ""); - assert!(!output.1); - - let output = collapse_and_transform_whitespace("\n ", WhiteSpace::Normal, false); - assert_eq!(output.0, " "); - assert!(!output.1); -} diff --git a/components/layout_2020/flow/inline.rs b/components/layout_2020/flow/inline.rs index b90ee2857ed..8fd62d164b6 100644 --- a/components/layout_2020/flow/inline.rs +++ b/components/layout_2020/flow/inline.rs @@ -64,15 +64,6 @@ pub(crate) struct InlineFormattingContext { /// Whether or not this [`InlineFormattingContext`] contains floats. pub(super) contains_floats: bool, - - /// Whether this IFC being constructed currently ends with whitespace. This is used to - /// implement rule 4 of : - /// - /// > 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.) - pub(super) ends_with_whitespace: bool, } /// A collection of data used to cache [`FontMetrics`] in the [`InlineFormattingContext`] @@ -1355,7 +1346,6 @@ impl InlineFormattingContext { pub(super) fn new( text_decoration_line: TextDecorationLine, has_first_formatted_line: bool, - ends_with_whitespace: bool, ) -> InlineFormattingContext { InlineFormattingContext { inline_level_boxes: Default::default(), @@ -1363,7 +1353,6 @@ impl InlineFormattingContext { text_decoration_line, has_first_formatted_line, contains_floats: false, - ends_with_whitespace, } } @@ -1565,7 +1554,7 @@ impl InlineFormattingContext { fn inline_level_box_is_empty(inline_level_box: &InlineLevelBox) -> bool { match inline_level_box { InlineLevelBox::InlineBox(_) => false, - InlineLevelBox::TextRun(text_run) => !text_run.has_uncollapsible_content, + InlineLevelBox::TextRun(text_run) => !text_run.has_uncollapsible_content(), InlineLevelBox::OutOfFlowAbsolutelyPositionedBox(_) => false, InlineLevelBox::OutOfFlowFloatBox(_) => false, InlineLevelBox::Atomic(_) => false, @@ -1579,13 +1568,28 @@ impl InlineFormattingContext { /// all font matching and FontMetrics collection. pub(crate) fn break_and_shape_text(&mut self, layout_context: &LayoutContext) { let mut ifc_fonts = Vec::new(); + + // Whether the last processed node ended with whitespace. This is used to + // implement rule 4 of : + // + // > 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.) + let mut last_inline_box_ended_with_white_space = false; + crate::context::with_thread_local_font_context(layout_context, |font_context| { let mut linebreaker = None; self.foreach(|iter_item| match iter_item { InlineFormattingContextIterItem::Item(InlineLevelBox::TextRun( ref mut text_run, )) => { - text_run.break_and_shape(font_context, &mut linebreaker, &mut ifc_fonts); + text_run.break_and_shape( + font_context, + &mut linebreaker, + &mut ifc_fonts, + &mut last_inline_box_ended_with_white_space, + ); }, InlineFormattingContextIterItem::Item(InlineLevelBox::InlineBox(inline_box)) => { if let Some(font) = @@ -1595,6 +1599,9 @@ impl InlineFormattingContext { Some(add_or_get_font(&font, &mut ifc_fonts)); } }, + InlineFormattingContextIterItem::Item(InlineLevelBox::Atomic(_)) => { + last_inline_box_ended_with_white_space = false; + }, _ => {}, }); }); diff --git a/components/layout_2020/flow/text_run.rs b/components/layout_2020/flow/text_run.rs index 415317cffe9..26a8c4db7e9 100644 --- a/components/layout_2020/flow/text_run.rs +++ b/components/layout_2020/flow/text_run.rs @@ -3,6 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::mem; +use std::str::Chars; use app_units::Au; use gfx::font::{FontRef, ShapingFlags, ShapingOptions}; @@ -15,6 +16,7 @@ use range::Range; use serde::Serialize; use servo_arc::Arc; use style::computed_values::text_rendering::T as TextRendering; +use style::computed_values::white_space::T as WhiteSpace; use style::computed_values::word_break::T as WordBreak; use style::properties::ComputedValues; use unicode_script::Script; @@ -43,10 +45,6 @@ pub(crate) struct TextRun { /// segments, and shaped. pub shaped_text: Vec, - /// Whether or not this [`TextRun`] has uncollapsible content. This is used - /// to determine if an [`super::InlineFormattingContext`] is considered empty or not. - pub has_uncollapsible_content: bool, - /// Whether or not to prevent a soft wrap opportunity at the start of this [`TextRun`]. /// This depends on the whether the first character in the run prevents a soft wrap /// opportunity. @@ -174,26 +172,49 @@ impl TextRun { base_fragment_info: BaseFragmentInfo, parent_style: Arc, text: String, - has_uncollapsible_content: bool, ) -> Self { Self { base_fragment_info, parent_style, text, - has_uncollapsible_content, shaped_text: Vec::new(), prevent_soft_wrap_opportunity_at_start: false, prevent_soft_wrap_opportunity_at_end: false, } } + /// Whether or not this [`TextRun`] has uncollapsible content. This is used + /// to determine if an [`super::InlineFormattingContext`] is considered empty or not. + pub(super) fn has_uncollapsible_content(&self) -> bool { + let white_space = self.parent_style.clone_white_space(); + if white_space.preserve_spaces() && !self.text.is_empty() { + return true; + } + + for character in self.text.chars() { + if !character.is_ascii_whitespace() { + return true; + } + if character == '\n' && white_space.preserve_newlines() { + return true; + } + } + + false + } + pub(super) fn break_and_shape( &mut self, font_context: &mut FontContext, linebreaker: &mut Option, font_cache: &mut Vec, + last_inline_box_ended_with_white_space: &mut bool, ) { - let segment_results = self.segment_text(font_context, font_cache); + let segment_results = self.segment_text( + font_context, + font_cache, + last_inline_box_ended_with_white_space, + ); let inherited_text_style = self.parent_style.get_inherited_text().clone(); let letter_spacing = if inherited_text_style.letter_spacing.0.px() != 0. { Some(app_units::Au::from(inherited_text_style.letter_spacing.0)) @@ -256,54 +277,76 @@ impl TextRun { &mut self, font_context: &mut FontContext, font_cache: &mut Vec, + last_inline_box_ended_with_white_space: &mut bool, ) -> Vec<(TextRunSegment, FontRef)> { let font_group = font_context.font_group(self.parent_style.clone_font()); let mut current: Option<(TextRunSegment, FontRef)> = None; let mut results = Vec::new(); - for (byte_index, character) in self.text.char_indices() { - let prevents_soft_wrap_opportunity = - char_prevents_soft_wrap_opportunity_when_before_or_after_atomic(character); - if byte_index == 0 && prevents_soft_wrap_opportunity { - self.prevent_soft_wrap_opportunity_at_start = true; - } - self.prevent_soft_wrap_opportunity_at_end = prevents_soft_wrap_opportunity; + let text = std::mem::replace(&mut self.text, String::new()); + let collapsed = WhitespaceCollapse::new( + text.as_str(), + self.parent_style.clone_white_space(), + *last_inline_box_ended_with_white_space, + ); - if char_does_not_change_font(character) { - continue; - } + let mut next_byte_index = 0; + let text = collapsed + .map(|character| { + let current_byte_index = next_byte_index; + next_byte_index += character.len_utf8(); - let font = match font_group - .borrow_mut() - .find_by_codepoint(font_context, character) - { - Some(font) => font, - None => continue, - }; - - // If the existing segment is compatible with the character, keep going. - let script = Script::from(character); - if let Some(current) = current.as_mut() { - if current.0.update_if_compatible(&font, script, font_cache) { - continue; + *last_inline_box_ended_with_white_space = character.is_whitespace(); + let prevents_soft_wrap_opportunity = + char_prevents_soft_wrap_opportunity_when_before_or_after_atomic(character); + if current_byte_index == 0 && prevents_soft_wrap_opportunity { + self.prevent_soft_wrap_opportunity_at_start = true; } - } + self.prevent_soft_wrap_opportunity_at_end = prevents_soft_wrap_opportunity; - let font_index = add_or_get_font(&font, font_cache); + if char_does_not_change_font(character) { + return character; + } - // Add the new segment and finish the existing one, if we had one. If the first - // characters in the run were control characters we may be creating the first - // segment in the middle of the run (ie the start should be 0). - let byte_index = match current { - Some(_) => ByteIndex(byte_index as isize), - None => ByteIndex(0 as isize), - }; - let new = (TextRunSegment::new(font_index, script, byte_index), font); - if let Some(mut finished) = current.replace(new) { - finished.0.range.extend_to(byte_index); - results.push(finished); - } - } + let font = match font_group + .borrow_mut() + .find_by_codepoint(font_context, character) + { + Some(font) => font, + None => return character, + }; + + // If the existing segment is compatible with the character, keep going. + let script = Script::from(character); + if let Some(current) = current.as_mut() { + if current.0.update_if_compatible(&font, script, font_cache) { + return character; + } + } + + let font_index = add_or_get_font(&font, font_cache); + + // Add the new segment and finish the existing one, if we had one. If the first + // characters in the run were control characters we may be creating the first + // segment in the middle of the run (ie the start should be 0). + let start_byte_index = match current { + Some(_) => ByteIndex(current_byte_index as isize), + None => ByteIndex(0 as isize), + }; + let new = ( + TextRunSegment::new(font_index, script, start_byte_index), + font, + ); + if let Some(mut finished) = current.replace(new) { + finished.0.range.extend_to(start_byte_index); + results.push(finished); + } + + character + }) + .collect(); + + let _ = std::mem::replace(&mut self.text, text); // Either we have a current segment or we only had control character and whitespace. In both // of those cases, just use the first font. @@ -443,3 +486,162 @@ pub(super) fn get_font_for_first_font_for_style( } font } + +fn preserve_segment_break() -> bool { + true +} + +pub struct WhitespaceCollapse<'a> { + char_iterator: Chars<'a>, + white_space: WhiteSpace, + + /// 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 [`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, +} + +impl<'a> WhitespaceCollapse<'a> { + pub fn new(input: &'a str, white_space: WhiteSpace, trim_beginning_white_space: bool) -> Self { + Self { + char_iterator: input.chars(), + white_space, + 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<'a> Iterator for WhitespaceCollapse<'a> { + type Item = char; + + fn next(&mut self) -> Option { + // 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.preserve_spaces() { + return self.char_iterator.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 + // (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 == WhiteSpace::PreLine { + 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) { + self.char_iterator.size_hint() + } + + fn count(self) -> usize + where + Self: Sized, + { + self.char_iterator.count() + } +} diff --git a/components/layout_2020/tests/text.rs b/components/layout_2020/tests/text.rs new file mode 100644 index 00000000000..894ccff277d --- /dev/null +++ b/components/layout_2020/tests/text.rs @@ -0,0 +1,55 @@ +/* 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/. */ + +mod text { + use layout_2020::flow::text_run::WhitespaceCollapse; + use style::computed_values::white_space::T as WhiteSpace; + + #[test] + fn test_collapse_whitespace() { + let collapse = |input, white_space, trim_beginning_white_space| { + WhitespaceCollapse::new(input, white_space, trim_beginning_white_space) + .collect::() + }; + + let output = collapse("H ", WhiteSpace::Normal, false); + assert_eq!(output, "H "); + + let output = collapse(" W", WhiteSpace::Normal, true); + assert_eq!(output, "W"); + + let output = collapse(" W", WhiteSpace::Normal, false); + assert_eq!(output, " W"); + + let output = collapse(" H W", WhiteSpace::Normal, false); + assert_eq!(output, " H W"); + + let output = collapse("\n H \n \t W", WhiteSpace::Normal, false); + assert_eq!(output, " H W"); + + let output = collapse("\n H \n \t W \n", WhiteSpace::Pre, false); + assert_eq!(output, "\n H \n \t W \n"); + + let output = collapse("\n H \n \t W \n ", WhiteSpace::PreLine, false); + assert_eq!(output, "\nH\nW\n"); + + let output = collapse("Hello \n World", WhiteSpace::PreLine, true); + assert_eq!(output, "Hello\nWorld"); + + let output = collapse(" \n World", WhiteSpace::PreLine, true); + assert_eq!(output, "\nWorld"); + + let output = collapse(" ", WhiteSpace::Normal, true); + assert_eq!(output, ""); + + let output = collapse(" ", WhiteSpace::Normal, false); + assert_eq!(output, " "); + + let output = collapse("\n ", WhiteSpace::Normal, true); + assert_eq!(output, ""); + + let output = collapse("\n ", WhiteSpace::Normal, false); + assert_eq!(output, " "); + } +}