layout: Linebreak the entire InlineFormattingContext at once (#32483)

Instead of linebreaking inside each single-font text segment, linebreak
the entire inline formatting context at once. This has several benefits:

1. It allows us to use `icu_segmenter` (already in use from style),
   which is written against a newer version of the Unicode spec --
   preventing breaking emoji clusters.
2. Opens up the possibility of changing the way that linebreaking and
   shaping work -- eventually allowing shaping across inline box
   boundaries and line breaking *after* shaping.

Co-authored-by: Rakhi Sharma <atbrakhi@igalia.com>
This commit is contained in:
Martin Robinson 2024-06-13 22:12:14 +02:00 committed by GitHub
parent 43a7dd5da0
commit bae9f6d844
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 356 additions and 220 deletions

1
Cargo.lock generated
View file

@ -3338,6 +3338,7 @@ dependencies = [
"gfx",
"gfx_traits",
"html5ever",
"icu_segmenter",
"ipc-channel",
"lazy_static",
"log",

View file

@ -55,6 +55,7 @@ http = "0.2"
hyper = "0.14"
hyper-rustls = { version = "0.24", default-features = false, features = ["acceptor", "http1", "http2", "logging", "tls12", "webpki-tokio"] }
hyper_serde = { path = "components/hyper_serde" }
icu_segmenter = "1.5.0"
image = "0.24"
imsz = "0.2"
indexmap = { version = "2.2.6", features = ["std"] }

View file

@ -9,7 +9,7 @@ publish = false
[lib]
name = "layout_2020"
path = "lib.rs"
test = false
test = true
doctest = false
[dependencies]
@ -26,6 +26,7 @@ fxhash = { workspace = true }
gfx = { path = "../gfx" }
gfx_traits = { workspace = true }
html5ever = { workspace = true }
icu_segmenter = { workspace = true }
ipc-channel = { workspace = true }
log = { workspace = true }
net_traits = { workspace = true }

View file

@ -131,8 +131,16 @@ impl InlineFormattingContextBuilder {
ArcRefCell::new(InlineLevelBox::Atomic(independent_formatting_context));
self.current_inline_level_boxes()
.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.
let string_to_push = "\u{fffc}";
self.text_segments.push(string_to_push.to_owned());
self.current_text_offset += string_to_push.len();
self.last_inline_box_ended_with_collapsible_white_space = false;
self.on_word_boundary = true;
inline_level_box
}

View file

@ -0,0 +1,120 @@
/* 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::ops::Range;
use icu_segmenter::LineSegmenter;
pub(crate) struct LineBreaker {
linebreaks: Vec<usize>,
current_offset: usize,
}
impl LineBreaker {
pub(crate) fn new(string: &str) -> Self {
let line_segmenter = LineSegmenter::new_auto();
Self {
// From https://docs.rs/icu_segmenter/1.5.0/icu_segmenter/struct.LineSegmenter.html
// > For consistency with the grapheme, word, and sentence segmenters, there is always a
// > breakpoint returned at index 0, but this breakpoint is not a meaningful line break
// > opportunity.
//
// Skip this first line break opportunity, as it isn't interesting to us.
linebreaks: line_segmenter.segment_str(string).skip(1).collect(),
current_offset: 0,
}
}
pub(crate) fn advance_to_linebreaks_in_range(&mut self, text_range: Range<usize>) -> &[usize] {
let linebreaks_in_range = self.linebreaks_in_range_after_current_offset(text_range);
self.current_offset = linebreaks_in_range.end;
&self.linebreaks[linebreaks_in_range]
}
fn linebreaks_in_range_after_current_offset(&self, text_range: Range<usize>) -> Range<usize> {
assert!(text_range.start <= text_range.end);
let mut linebreaks_range = self.current_offset..self.linebreaks.len();
while self.linebreaks[linebreaks_range.start] < text_range.start &&
linebreaks_range.len() > 1
{
linebreaks_range.start += 1;
}
let mut ending_linebreak_index = linebreaks_range.start;
while self.linebreaks[ending_linebreak_index] < text_range.end &&
ending_linebreak_index < self.linebreaks.len() - 1
{
ending_linebreak_index += 1;
}
linebreaks_range.end = ending_linebreak_index;
linebreaks_range
}
}
#[test]
fn test_linebreaker_ranges() {
let linebreaker = LineBreaker::new("abc def");
assert_eq!(linebreaker.linebreaks, [4, 7]);
assert_eq!(
linebreaker.linebreaks_in_range_after_current_offset(0..5),
0..1
);
// The last linebreak should not be included for the text range we are interested in.
assert_eq!(
linebreaker.linebreaks_in_range_after_current_offset(0..7),
0..1
);
let linebreaker = LineBreaker::new("abc d def");
assert_eq!(linebreaker.linebreaks, [4, 6, 9]);
assert_eq!(
linebreaker.linebreaks_in_range_after_current_offset(0..5),
0..1
);
assert_eq!(
linebreaker.linebreaks_in_range_after_current_offset(0..7),
0..2
);
assert_eq!(
linebreaker.linebreaks_in_range_after_current_offset(0..9),
0..2
);
assert_eq!(
linebreaker.linebreaks_in_range_after_current_offset(4..9),
0..2
);
std::panic::catch_unwind(|| {
let linebreaker = LineBreaker::new("abc def");
linebreaker.linebreaks_in_range_after_current_offset(5..2);
})
.expect_err("Reversed range should cause an assertion failure.");
}
#[test]
fn test_linebreaker_stateful_advance() {
let mut linebreaker = LineBreaker::new("abc d def");
assert_eq!(linebreaker.linebreaks, [4, 6, 9]);
assert!(linebreaker.advance_to_linebreaks_in_range(0..7) == &[4, 6]);
assert!(linebreaker.advance_to_linebreaks_in_range(8..9).is_empty());
// We've already advanced, so a range from the beginning shouldn't affect things.
assert!(linebreaker.advance_to_linebreaks_in_range(0..9).is_empty());
linebreaker.current_offset = 0;
// Sending a value out of range shoudn't break things.
assert!(linebreaker.advance_to_linebreaks_in_range(0..999) == &[4, 6]);
linebreaker.current_offset = 0;
std::panic::catch_unwind(|| {
let mut linebreaker = LineBreaker::new("abc d def");
linebreaker.advance_to_linebreaks_in_range(2..0);
})
.expect_err("Reversed range should cause an assertion failure.");
}

View file

@ -70,6 +70,7 @@
pub mod construct;
pub mod line;
mod line_breaker;
pub mod text_run;
use std::cell::OnceCell;
@ -84,6 +85,7 @@ use line::{
layout_line_items, AbsolutelyPositionedLineItem, AtomicLineItem, FloatLineItem,
InlineBoxLineItem, LineItem, LineItemLayoutState, LineMetrics, TextRunLineItem,
};
use line_breaker::LineBreaker;
use serde::Serialize;
use servo_arc::Arc;
use style::computed_values::text_wrap_mode::T as TextWrapMode;
@ -1593,13 +1595,13 @@ impl InlineFormattingContext {
let text_content: String = builder.text_segments.into_iter().collect();
let mut font_metrics = Vec::new();
let mut linebreaker = None;
let mut new_linebreaker = LineBreaker::new(text_content.as_str());
inline_formatting_context.foreach(|iter_item| match iter_item {
InlineFormattingContextIterItem::Item(InlineLevelBox::TextRun(ref mut text_run)) => {
text_run.break_and_shape(
&text_content[text_run.text_range.clone()],
text_run.segment_and_shape(
&text_content,
&layout_context.font_context,
&mut linebreaker,
&mut new_linebreaker,
&mut font_metrics,
);
},

View file

@ -18,13 +18,13 @@ use servo_arc::Arc;
use style::computed_values::text_rendering::T as TextRendering;
use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
use style::computed_values::word_break::T as WordBreak;
use style::properties::style_structs::InheritedText;
use style::properties::ComputedValues;
use style::str::char_is_whitespace;
use style::values::computed::OverflowWrap;
use unicode_script::Script;
use xi_unicode::{linebreak_property, LineBreakLeafIter};
use xi_unicode::linebreak_property;
use super::line_breaker::LineBreaker;
use super::{FontKeyAndMetrics, InlineFormattingContextState};
use crate::fragment_tree::BaseFragmentInfo;
@ -84,8 +84,8 @@ pub(crate) struct TextRunSegment {
#[serde(skip_serializing)]
pub script: Script,
/// The range of bytes in the [`TextRun`]'s text that this segment covers.
pub range: ServoRange<ByteIndex>,
/// The range of bytes in the parent [`super::InlineFormattingContext`]'s text content.
pub range: Range<usize>,
/// Whether or not the linebreaker said that we should allow a line break at the start of this
/// segment.
@ -96,11 +96,11 @@ pub(crate) struct TextRunSegment {
}
impl TextRunSegment {
fn new(font_index: usize, script: Script, byte_index: ByteIndex) -> Self {
fn new(font_index: usize, script: Script, start_offset: usize) -> Self {
Self {
script,
font_index,
range: ServoRange::new(byte_index, ByteIndex(0)),
range: start_offset..start_offset,
runs: Vec::new(),
break_at_start: false,
}
@ -167,6 +167,158 @@ impl TextRunSegment {
);
}
}
fn shape_and_push_range(
&mut self,
range: &Range<usize>,
formatting_context_text: &str,
segment_font: &FontRef,
options: &ShapingOptions,
) {
self.runs.push(GlyphRun {
glyph_store: segment_font.shape_text(&formatting_context_text[range.clone()], options),
range: ServoRange::new(
ByteIndex(range.start as isize),
ByteIndex(range.len() as isize),
),
});
}
/// Shape the text of this [`TextRunSegment`], first finding "words" for the shaper by processing
/// the linebreaks found in the owning [`super::InlineFormattingContext`]. Linebreaks are filtered,
/// based on the style of the parent inline box.
fn shape_text(
&mut self,
parent_style: &ComputedValues,
formatting_context_text: &str,
linebreaker: &mut LineBreaker,
shaping_options: &ShapingOptions,
font: FontRef,
) {
// Gather the linebreaks that apply to this segment from the inline formatting context's collection
// of line breaks. Also add a simulated break at the end of the segment in order to ensure the final
// piece of text is processed.
let range = self.range.clone();
let linebreaks = linebreaker.advance_to_linebreaks_in_range(self.range.clone());
let linebreak_iter = linebreaks.iter().chain(std::iter::once(&range.end));
self.runs.clear();
self.runs.reserve(linebreaks.len());
self.break_at_start = false;
let text_style = parent_style.get_inherited_text().clone();
let can_break_anywhere = text_style.word_break == WordBreak::BreakAll ||
text_style.overflow_wrap == OverflowWrap::Anywhere ||
text_style.overflow_wrap == OverflowWrap::BreakWord;
let mut last_slice_end = self.range.start;
for break_index in linebreak_iter {
if *break_index == self.range.start {
self.break_at_start = true;
continue;
}
// Extend the slice to the next UAX#14 line break opportunity.
let mut slice = last_slice_end..*break_index;
let word = &formatting_context_text[slice.clone()];
// Split off any trailing whitespace into a separate glyph run.
let mut whitespace = slice.end..slice.end;
let mut rev_char_indices = word.char_indices().rev().peekable();
let ends_with_newline = rev_char_indices
.peek()
.map_or(false, |&(_, character)| character == '\n');
if let Some((first_white_space_index, first_white_space_character)) = rev_char_indices
.take_while(|&(_, character)| char_is_whitespace(character))
.last()
{
whitespace.start = slice.start + first_white_space_index;
// If line breaking for a piece of text that has `white-space-collapse: break-spaces` there
// is a line break opportunity *after* every preserved space, but not before. This means
// that we should not split off the first whitespace, unless that white-space is a preserved
// newline.
//
// An exception to this is if the style tells us that we can break in the middle of words.
if text_style.white_space_collapse == WhiteSpaceCollapse::BreakSpaces &&
first_white_space_character != '\n' &&
!can_break_anywhere
{
whitespace.start += first_white_space_character.len_utf8();
}
slice.end = whitespace.start;
}
// If there's no whitespace and `word-break` is set to `keep-all`, try increasing the slice.
// TODO: This should only happen for CJK text.
let can_break_anywhere = text_style.word_break == WordBreak::BreakAll ||
text_style.overflow_wrap == OverflowWrap::Anywhere ||
text_style.overflow_wrap == OverflowWrap::BreakWord;
if whitespace.is_empty() &&
*break_index != self.range.end &&
text_style.word_break == WordBreak::KeepAll &&
!can_break_anywhere
{
continue;
}
// Only advance the last_slice_end if we are not going to try to expand the slice.
last_slice_end = *break_index;
// Push the non-whitespace part of the range.
if !slice.is_empty() {
self.shape_and_push_range(&slice, formatting_context_text, &font, shaping_options);
}
if whitespace.is_empty() {
continue;
}
let mut options = *shaping_options;
options
.flags
.insert(ShapingFlags::IS_WHITESPACE_SHAPING_FLAG);
// If `white-space-collapse: break-spaces` is active, insert a line breaking opportunity
// between each white space character in the white space that we trimmed off.
if text_style.white_space_collapse == WhiteSpaceCollapse::BreakSpaces {
let start_index = whitespace.start;
for (index, character) in formatting_context_text[whitespace].char_indices() {
let index = start_index + index;
self.shape_and_push_range(
&(index..index + character.len_utf8()),
formatting_context_text,
&font,
&options,
);
}
continue;
}
// The breaker breaks after every newline, so either there is none,
// or there is exactly one at the very end. In the latter case,
// split it into a different run. That's because shaping considers
// a newline to have the same advance as a space, but during layout
// we want to treat the newline as having no advance.
if ends_with_newline && whitespace.len() > 1 {
self.shape_and_push_range(
&(whitespace.start..whitespace.end - 1),
formatting_context_text,
&font,
&options,
);
self.shape_and_push_range(
&(whitespace.end - 1..whitespace.end),
formatting_context_text,
&font,
&options,
);
} else {
self.shape_and_push_range(&whitespace, formatting_context_text, &font, &options);
}
}
}
}
impl TextRun {
@ -185,14 +337,13 @@ impl TextRun {
}
}
pub(super) fn break_and_shape(
pub(super) fn segment_and_shape(
&mut self,
text_content: &str,
formatting_context_text: &str,
font_context: &FontContext<FontCacheThread>,
linebreaker: &mut Option<LineBreakLeafIter>,
linebreaker: &mut LineBreaker,
font_cache: &mut Vec<FontKeyAndMetrics>,
) {
let segment_results = self.segment_text(text_content, font_context, font_cache);
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))
@ -208,13 +359,12 @@ impl TextRun {
flags.insert(ShapingFlags::IGNORE_LIGATURES_SHAPING_FLAG);
flags.insert(ShapingFlags::DISABLE_KERNING_SHAPING_FLAG)
}
if inherited_text_style.word_break == WordBreak::KeepAll {
flags.insert(ShapingFlags::KEEP_ALL_FLAG);
}
let specified_word_spacing = &inherited_text_style.word_spacing;
let style_word_spacing: Option<Au> = specified_word_spacing.to_length().map(|l| l.into());
let segments = segment_results
let segments = self
.segment_text_by_font(formatting_context_text, font_context, font_cache)
.into_iter()
.map(|(mut segment, font)| {
let word_spacing = style_word_spacing.unwrap_or_else(|| {
@ -224,20 +374,21 @@ impl TextRun {
.unwrap_or(gfx::font::LAST_RESORT_GLYPH_ADVANCE);
specified_word_spacing.to_used_value(Au::from_f64_px(space_width))
});
let shaping_options = ShapingOptions {
letter_spacing,
word_spacing,
script: segment.script,
flags,
};
(segment.runs, segment.break_at_start) = break_and_shape(
font,
&text_content[segment.range.begin().0 as usize..segment.range.end().0 as usize],
&inherited_text_style,
&shaping_options,
linebreaker,
);
segment.shape_text(
&self.parent_style,
formatting_context_text,
linebreaker,
&shaping_options,
font,
);
segment
})
.collect();
@ -249,9 +400,9 @@ impl TextRun {
/// font and script. Fonts may differ when glyphs are found in fallback fonts. Fonts are stored
/// in the `font_cache` which is a cache of all font keys and metrics used in this
/// [`super::InlineFormattingContext`].
fn segment_text(
fn segment_text_by_font(
&mut self,
text_content: &str,
formatting_context_text: &str,
font_context: &FontContext<FontCacheThread>,
font_cache: &mut Vec<FontKeyAndMetrics>,
) -> Vec<(TextRunSegment, FontRef)> {
@ -259,15 +410,16 @@ impl TextRun {
let mut current: Option<(TextRunSegment, FontRef)> = None;
let mut results = Vec::new();
let char_iterator = TwoCharsAtATimeIterator::new(text_content.chars());
let mut next_byte_index = 0;
let text_run_text = &formatting_context_text[self.text_range.clone()];
let char_iterator = TwoCharsAtATimeIterator::new(text_run_text.chars());
let mut next_byte_index = self.text_range.start;
for (character, next_character) in char_iterator {
let current_byte_index = next_byte_index;
next_byte_index += character.len_utf8();
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 {
if current_byte_index == self.text_range.start && prevents_soft_wrap_opportunity {
self.prevent_soft_wrap_opportunity_at_start = true;
}
self.prevent_soft_wrap_opportunity_at_end = prevents_soft_wrap_opportunity;
@ -296,17 +448,19 @@ impl TextRun {
// 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).
// segment in the middle of the run (ie the start should be the start of this
// text run's text).
let start_byte_index = match current {
Some(_) => ByteIndex(current_byte_index as isize),
None => ByteIndex(0_isize),
Some(_) => current_byte_index,
None => self.text_range.start,
};
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);
// The end of the previous segment is the start of the next one.
finished.0.range.end = current_byte_index;
results.push(finished);
}
}
@ -317,7 +471,7 @@ impl TextRun {
current = font_group.write().first(font_context).map(|font| {
let font_index = add_or_get_font(&font, font_cache);
(
TextRunSegment::new(font_index, Script::Common, ByteIndex(0)),
TextRunSegment::new(font_index, Script::Common, self.text_range.start),
font,
)
})
@ -325,10 +479,7 @@ impl TextRun {
// Extend the last segment to the end of the string and add it to the results.
if let Some(mut last_segment) = current.take() {
last_segment
.0
.range
.extend_to(ByteIndex(text_content.len() as isize));
last_segment.0.range.end = self.text_range.end;
results.push(last_segment);
}
@ -463,129 +614,3 @@ where
Some((character, self.next_character))
}
}
pub fn break_and_shape(
font: FontRef,
text: &str,
text_style: &InheritedText,
shaping_options: &ShapingOptions,
breaker: &mut Option<LineBreakLeafIter>,
) -> (Vec<GlyphRun>, bool) {
let mut glyphs = vec![];
if breaker.is_none() {
if text.is_empty() {
return (glyphs, true);
}
*breaker = Some(LineBreakLeafIter::new(text, 0));
}
let breaker = breaker.as_mut().unwrap();
let mut push_range = |range: &Range<usize>, options: &ShapingOptions| {
glyphs.push(GlyphRun {
glyph_store: font.shape_text(&text[range.clone()], options),
range: ServoRange::new(
ByteIndex(range.start as isize),
ByteIndex(range.len() as isize),
),
});
};
let can_break_anywhere = text_style.word_break == WordBreak::BreakAll ||
text_style.overflow_wrap == OverflowWrap::Anywhere ||
text_style.overflow_wrap == OverflowWrap::BreakWord;
let mut break_at_zero = false;
let mut last_slice_end = 0;
while last_slice_end != text.len() {
let (break_index, _is_hard_break) = breaker.next(text);
if break_index == 0 {
break_at_zero = true;
}
// Extend the slice to the next UAX#14 line break opportunity.
let mut slice = last_slice_end..break_index;
let word = &text[slice.clone()];
// Split off any trailing whitespace into a separate glyph run.
let mut whitespace = slice.end..slice.end;
let mut rev_char_indices = word.char_indices().rev().peekable();
let ends_with_newline = rev_char_indices.peek().map_or(false, |&(_, c)| c == '\n');
if let Some((first_white_space_index, first_white_space_character)) = rev_char_indices
.take_while(|&(_, c)| char_is_whitespace(c))
.last()
{
whitespace.start = slice.start + first_white_space_index;
// If line breaking for a piece of text that has `white-space-collapse: break-spaces` there
// is a line break opportunity *after* every preserved space, but not before. This means
// that we should not split off the first whitespace, unless that white-space is a preserved
// newline.
//
// An exception to this is if the style tells us that we can break in the middle of words.
if text_style.white_space_collapse == WhiteSpaceCollapse::BreakSpaces &&
first_white_space_character != '\n' &&
!can_break_anywhere
{
whitespace.start += first_white_space_character.len_utf8();
}
slice.end = whitespace.start;
}
// If there's no whitespace and `word-break` is set to `keep-all`, try increasing the slice.
// TODO: This should only happen for CJK text.
let can_break_anywhere = text_style.word_break == WordBreak::BreakAll ||
text_style.overflow_wrap == OverflowWrap::Anywhere ||
text_style.overflow_wrap == OverflowWrap::BreakWord;
if whitespace.is_empty() &&
break_index != text.len() &&
text_style.word_break == WordBreak::KeepAll &&
!can_break_anywhere
{
continue;
}
// Only advance the last_slice_end if we are not going to try to expand the slice.
last_slice_end = break_index;
// Push the non-whitespace part of the range.
if !slice.is_empty() {
push_range(&slice, shaping_options);
}
if whitespace.is_empty() {
continue;
}
let mut options = *shaping_options;
options
.flags
.insert(ShapingFlags::IS_WHITESPACE_SHAPING_FLAG);
// If `white-space-collapse: break-spaces` is active, insert a line breaking opportunity
// between each white space character in the white space that we trimmed off.
if text_style.white_space_collapse == WhiteSpaceCollapse::BreakSpaces {
let start_index = whitespace.start;
for (index, character) in text[whitespace].char_indices() {
let index = start_index + index;
push_range(&(index..index + character.len_utf8()), &options);
}
continue;
}
// The breaker breaks after every newline, so either there is none,
// or there is exactly one at the very end. In the latter case,
// split it into a different run. That's because shaping considers
// a newline to have the same advance as a space, but during layout
// we want to treat the newline as having no advance.
if ends_with_newline && whitespace.len() > 1 {
push_range(&(whitespace.start..whitespace.end - 1), &options);
push_range(&(whitespace.end - 1..whitespace.end), &options);
} else {
push_range(&whitespace, &options);
}
}
(glyphs, break_at_zero)
}

View file

@ -0,0 +1,2 @@
[line-break-anywhere-overrides-uax-behavior-015.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[line-break-normal-015b.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[line-break-strict-015b.xht]
expected: FAIL

View file

@ -1,2 +0,0 @@
[line-breaking-013.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[line-breaking-014.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[line-breaking-021.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[line-breaking-024.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[line-breaking-025.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[line-breaking-026.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[line-breaking-027.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[line-breaking-atomic-010.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[line-breaking-atomic-011.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[shaping-001-ref.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[shaping-002-ref.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[shaping-003-ref.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[shaping-008-ref.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[shaping-009-ref.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[shaping-010-ref.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[shaping-011-ref.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[shaping-014-ref.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[shaping-016-ref.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[shaping-020-ref.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[shaping-021-ref.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[shaping-022-ref.html]
expected: FAIL

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,2 @@
[word-break-manual-001.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[word-break-normal-002.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[word-break-normal-003.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[word-break-normal-th-000.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[word-break-normal-th-001.html]
expected: FAIL