Auto merge of #26447 - jdm:white-space-2020, r=nox

Layout 2020: Implement basic white-space: pre support

With these changes `<pre>` and `<br>` preserve spaces and force line breaks appropriately.

---
- [x] `./mach build -d` does not report any errors
- [x] `./mach test-tidy` does not report any errors
- [x] These changes fix #26440
- [x] There are tests for these changes
This commit is contained in:
bors-servo 2020-07-29 10:29:30 -04:00 committed by GitHub
commit 6a3c3a4e18
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 128 additions and 135 deletions

View file

@ -20,6 +20,7 @@ use rayon_croissant::ParallelIteratorExt;
use servo_arc::Arc; use servo_arc::Arc;
use std::borrow::Cow; use std::borrow::Cow;
use std::convert::{TryFrom, TryInto}; use std::convert::{TryFrom, TryInto};
use style::computed_values::white_space::T as WhiteSpace;
use style::properties::longhands::list_style_position::computed_value::T as ListStylePosition; use style::properties::longhands::list_style_position::computed_value::T as ListStylePosition;
use style::properties::ComputedValues; use style::properties::ComputedValues;
use style::selector_parser::PseudoElement; use style::selector_parser::PseudoElement;
@ -293,8 +294,15 @@ where
} }
fn handle_text(&mut self, info: &NodeAndStyleInfo<Node>, input: Cow<'dom, str>) { fn handle_text(&mut self, info: &NodeAndStyleInfo<Node>, input: Cow<'dom, str>) {
let (leading_whitespace, mut input) = self.handle_leading_whitespace(&input); // Skip any leading whitespace as dictated by the node's style.
if leading_whitespace || !input.is_empty() { let white_space = info.style.get_inherited_text().white_space;
let (preserved_leading_whitespace, mut input) =
self.handle_leading_whitespace(&input, white_space);
if !preserved_leading_whitespace && input.is_empty() {
return;
}
// This text node should be pushed either to the next ongoing // This text node should be pushed either to the next ongoing
// inline level box with the parent style of that inline level box // inline level box with the parent style of that inline level box
// that will be ended, or directly to the ongoing inline formatting // that will be ended, or directly to the ongoing inline formatting
@ -320,23 +328,53 @@ where
output = new_text_run_contents.as_mut().unwrap(); output = new_text_run_contents.as_mut().unwrap();
} }
if leading_whitespace { if preserved_leading_whitespace {
output.push(' ') output.push(' ')
} }
loop {
if let Some(i) = input.bytes().position(|b| b.is_ascii_whitespace()) { match (
white_space.preserve_spaces(),
white_space.preserve_newlines(),
) {
// All whitespace is significant, so we don't need to transform
// the input at all.
(true, true) => {
output.push_str(input);
},
// There are no cases in CSS where where need to preserve spaces
// but not newlines.
(true, false) => unreachable!(),
// Spaces are not significant, but newlines might be. We need
// to collapse non-significant whitespace as appropriate.
(false, preserve_newlines) => loop {
// If there are any spaces that need preserving, split the string
// that precedes them, collapse them into a single whitespace,
// then process the remainder of the string independently.
if let Some(i) = input
.bytes()
.position(|b| b.is_ascii_whitespace() && (!preserve_newlines || b != b'\n'))
{
let (non_whitespace, rest) = input.split_at(i); let (non_whitespace, rest) = input.split_at(i);
output.push_str(non_whitespace); output.push_str(non_whitespace);
output.push(' '); output.push(' ');
if let Some(i) = rest.bytes().position(|b| !b.is_ascii_whitespace()) {
// Find the first byte that is either significant whitespace or
// non-whitespace to continue processing it.
if let Some(i) = rest.bytes().position(|b| {
!b.is_ascii_whitespace() || (preserve_newlines && b == b'\n')
}) {
input = &rest[i..]; input = &rest[i..];
} else { } else {
break; break;
} }
} else { } else {
// No whitespace found, so no transformation is required.
output.push_str(input); output.push_str(input);
break; break;
} }
},
} }
} }
@ -349,7 +387,6 @@ where
} }
} }
} }
}
impl<'dom, Node> BlockContainerBuilder<'dom, '_, Node> impl<'dom, Node> BlockContainerBuilder<'dom, '_, Node>
where where
@ -359,10 +396,14 @@ where
/// ///
/// * Whether this text run has preserved (non-collapsible) leading whitespace /// * Whether this text run has preserved (non-collapsible) leading whitespace
/// * The contents starting at the first non-whitespace character (or the empty string) /// * The contents starting at the first non-whitespace character (or the empty string)
fn handle_leading_whitespace<'text>(&mut self, text: &'text str) -> (bool, &'text str) { fn handle_leading_whitespace<'text>(
&mut self,
text: &'text str,
white_space: WhiteSpace,
) -> (bool, &'text str) {
// FIXME: this is only an approximation of // FIXME: this is only an approximation of
// https://drafts.csswg.org/css2/text.html#white-space-model // https://drafts.csswg.org/css2/text.html#white-space-model
if !text.starts_with(|c: char| c.is_ascii_whitespace()) { if !text.starts_with(|c: char| c.is_ascii_whitespace()) || white_space.preserve_spaces() {
return (false, text); return (false, text);
} }

View file

@ -756,13 +756,21 @@ impl TextRun {
let mut glyphs = vec![]; let mut glyphs = vec![];
let mut advance_width = Length::zero(); let mut advance_width = Length::zero();
let mut last_break_opportunity = None; let mut last_break_opportunity = None;
let mut force_line_break = false;
// Fit as many glyphs within a single line as possible.
loop { loop {
let next = runs.next(); let next = runs.next();
// If there are no more text runs we still need to check if the last
// run was a forced line break
if next if next
.as_ref() .as_ref()
.map_or(true, |run| run.glyph_store.is_whitespace()) .map_or(true, |run| run.glyph_store.is_whitespace())
{ {
// If this run exceeds the bounds of the containing block, then
// we need to attempt to break the line.
if advance_width > ifc.containing_block.inline_size - ifc.inline_position { if advance_width > ifc.containing_block.inline_size - ifc.inline_position {
// Reset the text run iterator to the last whitespace if possible,
// to attempt to re-layout the most recent glyphs on a new line.
if let Some((len, width, iter)) = last_break_opportunity.take() { if let Some((len, width, iter)) = last_break_opportunity.take() {
glyphs.truncate(len); glyphs.truncate(len);
advance_width = width; advance_width = width;
@ -774,10 +782,24 @@ impl TextRun {
if let Some(run) = next { if let Some(run) = next {
if run.glyph_store.is_whitespace() { if run.glyph_store.is_whitespace() {
last_break_opportunity = Some((glyphs.len(), advance_width, runs.clone())); last_break_opportunity = Some((glyphs.len(), advance_width, runs.clone()));
// If this whitespace ends with a newline, we need to check if
// it's meaningful within the current style. If so, we force
// a line break immediately.
let last_byte = self.text.as_bytes().get(run.range.end().to_usize() - 1);
if last_byte == Some(&b'\n') &&
self.parent_style
.get_inherited_text()
.white_space
.preserve_newlines()
{
force_line_break = true;
break;
}
} }
glyphs.push(run.glyph_store.clone()); glyphs.push(run.glyph_store.clone());
advance_width += Length::from(run.glyph_store.total_advance()); advance_width += Length::from(run.glyph_store.total_advance());
} else { } else {
// No more runs, so we can end the line.
break; break;
} }
} }
@ -812,7 +834,8 @@ impl TextRun {
glyphs, glyphs,
text_decoration_line: ifc.current_nesting_level.text_decoration_line, text_decoration_line: ifc.current_nesting_level.text_decoration_line,
})); }));
if runs.as_slice().is_empty() { // If this line is being broken because of a trailing newline, we can't ignore it.
if runs.as_slice().is_empty() && !force_line_break {
break; break;
} else { } else {
// New line // New line

View file

@ -186,7 +186,6 @@ ${helpers.predefined_type(
name="white-space" name="white-space"
values="normal pre nowrap pre-wrap pre-line" values="normal pre nowrap pre-wrap pre-line"
engines="gecko servo-2013 servo-2020", engines="gecko servo-2013 servo-2020",
servo_2020_pref="layout.2020.unimplemented",
extra_gecko_values="break-spaces -moz-pre-space" extra_gecko_values="break-spaces -moz-pre-space"
gecko_enum_prefix="StyleWhiteSpace" gecko_enum_prefix="StyleWhiteSpace"
needs_conversion="True" needs_conversion="True"

View file

@ -1,2 +0,0 @@
[hypothetical-inline-alone-on-second-line.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[color-applies-to-008.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[color-applies-to-015.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[c562-white-sp-000.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[float-003.xht]
expected: FAIL

View file

@ -0,0 +1,2 @@
[float-no-content-beside-001.html]
expected: FAIL

View file

@ -0,0 +1,2 @@
[floats-placement-vertical-004.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[content-171.xht]
expected: FAIL

View file

@ -0,0 +1,2 @@
[content-173.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[content-newline-001.xht]
expected: FAIL

View file

@ -0,0 +1,2 @@
[content-white-space-002.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[padding-percentage-inherit-001.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[block-replaced-width-006.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[inline-replaced-width-001.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[inline-replaced-width-006.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[inlines-016.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[absolute-non-replaced-height-008.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[position-static-001.xht]
expected: FAIL

View file

@ -0,0 +1,2 @@
[table-anonymous-objects-009.xht]
expected: FAIL

View file

@ -0,0 +1,2 @@
[table-anonymous-objects-010.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[white-space-004.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[white-space-applies-to-003.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[white-space-processing-013.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[white-space-processing-016.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[white-space-processing-017.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[white-space-processing-018.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[white-space-processing-046.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[white-space-processing-047.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[white-space-processing-052.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[anonymous-boxes-001b.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[background-size-027.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[background-size-028.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[background-size-030.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[background-size-031.html]
expected: FAIL

View file

@ -0,0 +1,2 @@
[anonymous-flex-item-004.html]
expected: FAIL

View file

@ -0,0 +1,2 @@
[anonymous-flex-item-005.html]
expected: FAIL

View file

@ -0,0 +1,2 @@
[anonymous-flex-item-006.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[text-decoration-subelements-001.html]
expected: FAIL

View file

@ -23,15 +23,9 @@
[page-break-before: always] [page-break-before: always]
expected: FAIL expected: FAIL
[white-space: inherit]
expected: FAIL
[page-break-after: inherit] [page-break-after: inherit]
expected: FAIL expected: FAIL
[white-space: nowrap]
expected: FAIL
[page-break-before: left] [page-break-before: left]
expected: FAIL expected: FAIL
@ -215,9 +209,6 @@
[display: table-cell] [display: table-cell]
expected: FAIL expected: FAIL
[white-space: pre]
expected: FAIL
[text-indent: 5%] [text-indent: 5%]
expected: FAIL expected: FAIL
@ -257,9 +248,6 @@
[clear: both] [clear: both]
expected: FAIL expected: FAIL
[white-space: pre-wrap]
expected: FAIL
[outline-width: 0px] [outline-width: 0px]
expected: FAIL expected: FAIL
@ -326,9 +314,6 @@
[vertical-align: 1px] [vertical-align: 1px]
expected: FAIL expected: FAIL
[white-space: pre-line]
expected: FAIL
[display: table-column] [display: table-column]
expected: FAIL expected: FAIL
@ -386,9 +371,6 @@
[float: none] [float: none]
expected: FAIL expected: FAIL
[white-space: normal]
expected: FAIL
[list-style-type: lower-roman] [list-style-type: lower-roman]
expected: FAIL expected: FAIL

View file

@ -1,2 +0,0 @@
[br.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[input_whitespace.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[line_breaking_whitespace_collapse_a.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[many_brs_a.html]
expected: FAIL

View file

@ -0,0 +1,2 @@
[white-space-pre-line.htm]
expected: FAIL

View file

@ -1,2 +0,0 @@
[white_space_intrinsic_sizes_a.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[whitespace_pre.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[word-break-keep-all-005.htm]
expected: FAIL

View file

@ -1,2 +0,0 @@
[word-break-keep-all-006.htm]
expected: FAIL

View file

@ -1,2 +0,0 @@
[word-break-keep-all-007.htm]
expected: FAIL

View file

@ -0,0 +1,4 @@
[hit_test_multiple_sc.html]
[Hit testing works for following stacking contexts]
expected: FAIL