mirror of
https://github.com/servo/servo
synced 2026-05-09 00:22:16 +02:00
This and the accompanying stylo PR (https://github.com/servo/stylo/pull/320) is a partial implementation of the `first-letter` pseudo element. Testing: Existing WPT tests. There is one WPT test (fcp-typographic-pseudo.html) that servo currently fails would timeout with this PR because opacity of `TextFragment` is taken from the `InlineFormattingContext` instead of the pseudo element. Fixes: #43008 Part of: #15413 --------- Signed-off-by: Minghua Wu <michael.wu1107@gmail.com> Signed-off-by: minghuaw <michael.wu1107@gmail.com> Signed-off-by: Oriol Brufau <obrufau@igalia.com> Co-authored-by: Xiaocheng Hu <xiaochengh.work@gmail.com> Co-authored-by: Oriol Brufau <obrufau@igalia.com>
996 lines
38 KiB
Rust
996 lines
38 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 rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||
use servo_arc::Arc;
|
||
use style::properties::ComputedValues;
|
||
use style::properties::longhands::list_style_position::computed_value::T as ListStylePosition;
|
||
use style::selector_parser::PseudoElement;
|
||
use style::str::char_is_whitespace;
|
||
use style::values::specified::box_::DisplayOutside as StyloDisplayOutside;
|
||
use unicode_categories::UnicodeCategories;
|
||
|
||
use super::OutsideMarker;
|
||
use super::inline::construct::InlineFormattingContextBuilder;
|
||
use super::inline::inline_box::InlineBox;
|
||
use super::inline::{InlineFormattingContext, SharedInlineStyles};
|
||
use crate::PropagatedBoxTreeData;
|
||
use crate::cell::ArcRefCell;
|
||
use crate::context::LayoutContext;
|
||
use crate::dom::{BoxSlot, LayoutBox, NodeExt};
|
||
use crate::dom_traversal::{
|
||
Contents, NodeAndStyleInfo, NonReplacedContents, PseudoElementContentItem, TraversalHandler,
|
||
};
|
||
use crate::flow::float::FloatBox;
|
||
use crate::flow::{BlockContainer, BlockFormattingContext, BlockLevelBox};
|
||
use crate::formatting_contexts::{
|
||
IndependentFormattingContext, IndependentFormattingContextContents,
|
||
};
|
||
use crate::fragment_tree::FragmentFlags;
|
||
use crate::layout_box_base::LayoutBoxBase;
|
||
use crate::positioned::AbsolutelyPositionedBox;
|
||
use crate::style_ext::{ComputedValuesExt, DisplayGeneratingBox, DisplayInside, DisplayOutside};
|
||
use crate::table::{AnonymousTableContent, Table};
|
||
|
||
impl BlockFormattingContext {
|
||
pub(crate) fn construct(
|
||
context: &LayoutContext,
|
||
info: &NodeAndStyleInfo<'_>,
|
||
contents: NonReplacedContents,
|
||
propagated_data: PropagatedBoxTreeData,
|
||
is_list_item: bool,
|
||
) -> Self {
|
||
Self::from_block_container(BlockContainer::construct(
|
||
context,
|
||
info,
|
||
contents,
|
||
propagated_data,
|
||
is_list_item,
|
||
))
|
||
}
|
||
|
||
pub(crate) fn from_block_container(contents: BlockContainer) -> Self {
|
||
let contains_floats = contents.contains_floats();
|
||
Self {
|
||
contents,
|
||
contains_floats,
|
||
}
|
||
}
|
||
}
|
||
|
||
struct BlockLevelJob<'dom> {
|
||
info: NodeAndStyleInfo<'dom>,
|
||
box_slot: BoxSlot<'dom>,
|
||
propagated_data: PropagatedBoxTreeData,
|
||
kind: BlockLevelCreator,
|
||
}
|
||
|
||
pub(crate) enum BlockLevelCreator {
|
||
SameFormattingContextBlock(IntermediateBlockContainer),
|
||
Independent {
|
||
display_inside: DisplayInside,
|
||
contents: Contents,
|
||
},
|
||
OutOfFlowAbsolutelyPositionedBox {
|
||
display_inside: DisplayInside,
|
||
contents: Contents,
|
||
},
|
||
OutOfFlowFloatBox {
|
||
display_inside: DisplayInside,
|
||
contents: Contents,
|
||
},
|
||
OutsideMarker {
|
||
list_item_style: Arc<ComputedValues>,
|
||
contents: Vec<PseudoElementContentItem>,
|
||
},
|
||
AnonymousTable {
|
||
table_block: ArcRefCell<BlockLevelBox>,
|
||
},
|
||
}
|
||
|
||
impl BlockLevelCreator {
|
||
pub(crate) fn new_for_inflow_block_level_element<'dom>(
|
||
info: &NodeAndStyleInfo<'dom>,
|
||
display_inside: DisplayInside,
|
||
contents: Contents,
|
||
propagated_data: PropagatedBoxTreeData,
|
||
) -> Self {
|
||
match contents {
|
||
Contents::NonReplaced(contents) => match display_inside {
|
||
DisplayInside::Flow { is_list_item }
|
||
// Fragment flags are just used to indicate whether the element is replaced or a widget,
|
||
// and whether it's a body or root propagating its `overflow` to the viewport. We have
|
||
// already checked that the former is not the case.
|
||
// TODO(#39932): empty flags are wrong when propagating `overflow` to the viewport.
|
||
if !info.style.establishes_block_formatting_context(
|
||
FragmentFlags::empty()
|
||
) =>
|
||
{
|
||
Self::SameFormattingContextBlock(
|
||
IntermediateBlockContainer::Deferred {
|
||
contents,
|
||
propagated_data,
|
||
is_list_item,
|
||
},
|
||
)
|
||
},
|
||
_ => Self::Independent {
|
||
display_inside,
|
||
contents: Contents::NonReplaced(contents),
|
||
},
|
||
},
|
||
Contents::Replaced(_) | Contents::Widget(_) => Self::Independent {
|
||
display_inside,
|
||
contents,
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A block container that may still have to be constructed.
|
||
///
|
||
/// Represents either the inline formatting context of an anonymous block
|
||
/// box or the yet-to-be-computed block container generated from the children
|
||
/// of a given element.
|
||
///
|
||
/// Deferring allows using rayon’s `into_par_iter`.
|
||
pub(crate) enum IntermediateBlockContainer {
|
||
InlineFormattingContext(BlockContainer),
|
||
Deferred {
|
||
contents: NonReplacedContents,
|
||
propagated_data: PropagatedBoxTreeData,
|
||
is_list_item: bool,
|
||
},
|
||
}
|
||
|
||
/// A builder for a block container.
|
||
///
|
||
/// This builder starts from the first child of a given DOM node
|
||
/// and does a preorder traversal of all of its inclusive siblings.
|
||
pub(crate) struct BlockContainerBuilder<'dom, 'style> {
|
||
context: &'style LayoutContext<'style>,
|
||
|
||
/// This NodeAndStyleInfo contains the root node, the corresponding pseudo
|
||
/// content designator, and the block container style.
|
||
info: &'style NodeAndStyleInfo<'dom>,
|
||
|
||
/// The list of block-level boxes to be built for the final block container.
|
||
///
|
||
/// Contains all the block-level jobs we found traversing the tree
|
||
/// so far, if this is empty at the end of the traversal and the ongoing
|
||
/// inline formatting context is not empty, the block container establishes
|
||
/// an inline formatting context (see end of `build`).
|
||
///
|
||
/// DOM nodes which represent block-level boxes are immediately pushed
|
||
/// to this list with their style without ever being traversed at this
|
||
/// point, instead we just move to their next sibling. If the DOM node
|
||
/// doesn't have a next sibling, we either reached the end of the container
|
||
/// root or there are ongoing inline-level boxes
|
||
/// (see `handle_block_level_element`).
|
||
block_level_boxes: Vec<BlockLevelJob<'dom>>,
|
||
|
||
/// Whether or not this builder has yet produced a block which would be
|
||
/// be considered the first line for the purposes of `text-indent`.
|
||
have_already_seen_first_line_for_text_indent: bool,
|
||
|
||
/// The propagated data to use for BoxTree construction.
|
||
propagated_data: PropagatedBoxTreeData,
|
||
|
||
/// The [`InlineFormattingContextBuilder`] if we have encountered any inline items,
|
||
/// otherwise None.
|
||
///
|
||
/// TODO: This can be `OnceCell` once `OnceCell::get_mut_or_init` is stabilized.
|
||
inline_formatting_context_builder: Option<InlineFormattingContextBuilder>,
|
||
|
||
/// The [`NodeAndStyleInfo`] to use for anonymous block boxes pushed to the list of
|
||
/// block-level boxes, lazily initialized.
|
||
anonymous_box_info: Option<NodeAndStyleInfo<'dom>>,
|
||
|
||
/// A collection of content that is being added to an anonymous table. This is
|
||
/// composed of any sequence of internal table elements or table captions that
|
||
/// are found outside of a table.
|
||
anonymous_table_content: Vec<AnonymousTableContent<'dom>>,
|
||
|
||
/// Any [`InlineFormattingContexts`] created need to know about the ongoing `display: contents`
|
||
/// ancestors that have been processed. This `Vec` allows passing those into new
|
||
/// [`InlineFormattingContext`]s that we create.
|
||
display_contents_shared_styles: Vec<SharedInlineStyles>,
|
||
}
|
||
|
||
impl BlockContainer {
|
||
pub fn construct(
|
||
context: &LayoutContext,
|
||
info: &NodeAndStyleInfo<'_>,
|
||
contents: NonReplacedContents,
|
||
propagated_data: PropagatedBoxTreeData,
|
||
is_list_item: bool,
|
||
) -> BlockContainer {
|
||
let mut builder = BlockContainerBuilder::new(context, info, propagated_data);
|
||
|
||
if is_list_item {
|
||
if let Some((marker_info, marker_contents)) = crate::lists::make_marker(context, info) {
|
||
match marker_info.style.clone_list_style_position() {
|
||
ListStylePosition::Inside => {
|
||
builder.handle_list_item_marker_inside(&marker_info, marker_contents)
|
||
},
|
||
ListStylePosition::Outside => builder.handle_list_item_marker_outside(
|
||
&marker_info,
|
||
marker_contents,
|
||
info.style.clone(),
|
||
),
|
||
}
|
||
}
|
||
}
|
||
|
||
contents.traverse(context, info, &mut builder);
|
||
builder.finish()
|
||
}
|
||
}
|
||
|
||
impl<'dom, 'style> BlockContainerBuilder<'dom, 'style> {
|
||
pub(crate) fn new(
|
||
context: &'style LayoutContext,
|
||
info: &'style NodeAndStyleInfo<'dom>,
|
||
propagated_data: PropagatedBoxTreeData,
|
||
) -> Self {
|
||
BlockContainerBuilder {
|
||
context,
|
||
info,
|
||
block_level_boxes: Vec::new(),
|
||
propagated_data,
|
||
have_already_seen_first_line_for_text_indent: false,
|
||
anonymous_box_info: None,
|
||
anonymous_table_content: Vec::new(),
|
||
inline_formatting_context_builder: None,
|
||
display_contents_shared_styles: Vec::new(),
|
||
}
|
||
}
|
||
|
||
fn currently_processing_inline_box(&self) -> bool {
|
||
self.inline_formatting_context_builder
|
||
.as_ref()
|
||
.is_some_and(InlineFormattingContextBuilder::currently_processing_inline_box)
|
||
}
|
||
|
||
fn ensure_inline_formatting_context_builder(&mut self) -> &mut InlineFormattingContextBuilder {
|
||
self.inline_formatting_context_builder
|
||
.get_or_insert_with(|| {
|
||
let mut builder = InlineFormattingContextBuilder::new(self.info, self.context);
|
||
for shared_inline_styles in self.display_contents_shared_styles.iter() {
|
||
builder.enter_display_contents(shared_inline_styles.clone());
|
||
}
|
||
builder
|
||
})
|
||
}
|
||
|
||
fn finish_ongoing_inline_formatting_context(&mut self) -> Option<InlineFormattingContext> {
|
||
self.inline_formatting_context_builder.take()?.finish(
|
||
self.context,
|
||
!self.have_already_seen_first_line_for_text_indent,
|
||
self.info.node.is_single_line_text_input(),
|
||
self.info.style.to_bidi_level(),
|
||
)
|
||
}
|
||
|
||
pub(crate) fn finish(mut self) -> BlockContainer {
|
||
debug_assert!(!self.currently_processing_inline_box());
|
||
|
||
self.finish_anonymous_table_if_needed();
|
||
|
||
if let Some(inline_formatting_context) = self.finish_ongoing_inline_formatting_context() {
|
||
// There are two options here. This block was composed of both one or more inline formatting contexts
|
||
// and child blocks OR this block was a single inline formatting context. In the latter case, we
|
||
// just return the inline formatting context as the block itself.
|
||
if self.block_level_boxes.is_empty() {
|
||
return BlockContainer::InlineFormattingContext(inline_formatting_context);
|
||
}
|
||
self.push_block_level_job_for_inline_formatting_context(inline_formatting_context);
|
||
}
|
||
|
||
let context = self.context;
|
||
let block_level_boxes = if self.context.use_rayon {
|
||
self.block_level_boxes
|
||
.into_par_iter()
|
||
.map(|block_level_job| block_level_job.finish(context))
|
||
.collect()
|
||
} else {
|
||
self.block_level_boxes
|
||
.into_iter()
|
||
.map(|block_level_job| block_level_job.finish(context))
|
||
.collect()
|
||
};
|
||
|
||
BlockContainer::BlockLevelBoxes(block_level_boxes)
|
||
}
|
||
|
||
fn finish_anonymous_table_if_needed(&mut self) {
|
||
if self.anonymous_table_content.is_empty() {
|
||
return;
|
||
}
|
||
|
||
// From https://drafts.csswg.org/css-tables/#fixup-algorithm:
|
||
// > If the box’s parent is an inline, run-in, or ruby box (or any box that would perform
|
||
// > inlinification of its children), then an inline-table box must be generated; otherwise
|
||
// > it must be a table box.
|
||
//
|
||
// Note that text content in the inline formatting context isn't enough to force the
|
||
// creation of an inline table. It requires the parent to be an inline box.
|
||
let inline_table = self.currently_processing_inline_box();
|
||
|
||
let contents: Vec<AnonymousTableContent<'dom>> =
|
||
self.anonymous_table_content.drain(..).collect();
|
||
let last_text = match contents.last() {
|
||
Some(AnonymousTableContent::Text(info, text)) => Some((info.clone(), text.clone())),
|
||
_ => None,
|
||
};
|
||
|
||
let (table_info, ifc) =
|
||
Table::construct_anonymous(self.context, self.info, contents, self.propagated_data);
|
||
|
||
if inline_table {
|
||
self.ensure_inline_formatting_context_builder()
|
||
.push_atomic(|| ArcRefCell::new(ifc), None);
|
||
} else {
|
||
let table_block = ArcRefCell::new(BlockLevelBox::Independent(ifc));
|
||
|
||
if let Some(inline_formatting_context) = self.finish_ongoing_inline_formatting_context()
|
||
{
|
||
self.push_block_level_job_for_inline_formatting_context(inline_formatting_context);
|
||
}
|
||
|
||
let box_slot = table_info.node.box_slot();
|
||
self.block_level_boxes.push(BlockLevelJob {
|
||
info: table_info,
|
||
box_slot,
|
||
kind: BlockLevelCreator::AnonymousTable { table_block },
|
||
propagated_data: self.propagated_data,
|
||
});
|
||
}
|
||
|
||
// If the last element in the anonymous table content is whitespace, that
|
||
// whitespace doesn't actually belong to the table. It should be processed outside
|
||
// ie become a space between the anonymous table and the rest of the block
|
||
// content. Anonymous tables are really only constructed around internal table
|
||
// elements and the whitespace between them, so this trailing whitespace should
|
||
// not be included.
|
||
//
|
||
// See https://drafts.csswg.org/css-tables/#fixup-algorithm sections "Remove
|
||
// irrelevant boxes" and "Generate missing parents."
|
||
if let Some((info, text)) = last_text {
|
||
self.handle_text(&info, text);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Computes the range of the first letter.
|
||
///
|
||
/// The range includes any preceding punctuation, and any spaces interleaved
|
||
/// within the preceding punctuation or between the preceding punctuation
|
||
/// and the first letter/number/symbol.
|
||
/// Succeeding punctuation are included in the range, but any space
|
||
/// following the letter/number/symbol ends the range. Intervening
|
||
/// succeeding spaces are not supported yet.
|
||
///
|
||
/// <https://drafts.csswg.org/css-pseudo/#first-letter-pattern>
|
||
fn first_letter_range(text: &str) -> std::ops::Range<usize> {
|
||
use unicode_categories::UnicodeCategories;
|
||
|
||
enum State {
|
||
Start,
|
||
PrecedingPunc,
|
||
/// Unicode general category L: letter, N: number and S: symbol
|
||
Lns,
|
||
SucceedingPunc,
|
||
}
|
||
|
||
let mut start = 0;
|
||
let mut end = None;
|
||
let mut state = State::Start;
|
||
|
||
for (i, c) in text.char_indices() {
|
||
match &mut state {
|
||
State::Start => {
|
||
if c.is_punctuation() {
|
||
start = i;
|
||
state = State::PrecedingPunc;
|
||
} else if c.is_letter() || c.is_number() || c.is_symbol() {
|
||
start = i;
|
||
state = State::Lns;
|
||
} else if c.is_separator_space() {
|
||
continue;
|
||
} else {
|
||
// Found invalid character
|
||
return 0..0;
|
||
}
|
||
},
|
||
State::PrecedingPunc => {
|
||
if c.is_letter() || c.is_number() || c.is_symbol() {
|
||
state = State::Lns;
|
||
} else if c.is_punctuation() || (c.is_separator_space() && c != '\u{3000}') {
|
||
continue;
|
||
} else {
|
||
// Found invalid character
|
||
return 0..0;
|
||
}
|
||
},
|
||
State::Lns => {
|
||
// TODO: Implement support for intervening spaces
|
||
// <https://drafts.csswg.org/css-pseudo/#first-letter-pattern>
|
||
if c.is_punctuation() && !c.is_punctuation_open() && !c.is_punctuation_dash() {
|
||
state = State::SucceedingPunc;
|
||
} else {
|
||
end = Some(i);
|
||
break;
|
||
}
|
||
},
|
||
State::SucceedingPunc => {
|
||
// TODO: Implement support for intervening spaces
|
||
// <https://drafts.csswg.org/css-pseudo/#first-letter-pattern>
|
||
if c.is_punctuation() && !c.is_punctuation_open() && !c.is_punctuation_dash() {
|
||
continue;
|
||
} else {
|
||
end = Some(i);
|
||
break;
|
||
}
|
||
},
|
||
}
|
||
}
|
||
|
||
match state {
|
||
State::Start | State::PrecedingPunc => 0..0,
|
||
State::Lns | State::SucceedingPunc => {
|
||
let end = end.unwrap_or(text.len());
|
||
start..end
|
||
},
|
||
}
|
||
}
|
||
|
||
impl<'dom> TraversalHandler<'dom> for BlockContainerBuilder<'dom, '_> {
|
||
fn handle_element(
|
||
&mut self,
|
||
info: &NodeAndStyleInfo<'dom>,
|
||
display: DisplayGeneratingBox,
|
||
contents: Contents,
|
||
box_slot: BoxSlot<'dom>,
|
||
) {
|
||
match display {
|
||
DisplayGeneratingBox::OutsideInside { outside, inside } => {
|
||
self.finish_anonymous_table_if_needed();
|
||
|
||
match outside {
|
||
DisplayOutside::Inline => {
|
||
self.handle_inline_level_element(info, inside, contents, box_slot)
|
||
},
|
||
DisplayOutside::Block => {
|
||
let box_style = info.style.get_box();
|
||
// Floats and abspos cause blockification, so they only happen in this case.
|
||
// https://drafts.csswg.org/css2/visuren.html#dis-pos-flo
|
||
if box_style.position.is_absolutely_positioned() {
|
||
self.handle_absolutely_positioned_element(
|
||
info, inside, contents, box_slot,
|
||
)
|
||
} else if box_style.float.is_floating() {
|
||
self.handle_float_element(info, inside, contents, box_slot)
|
||
} else {
|
||
self.handle_block_level_element(info, inside, contents, box_slot)
|
||
}
|
||
},
|
||
};
|
||
},
|
||
DisplayGeneratingBox::LayoutInternal(_) => {
|
||
self.anonymous_table_content
|
||
.push(AnonymousTableContent::Element {
|
||
info: info.clone(),
|
||
display,
|
||
contents,
|
||
box_slot,
|
||
});
|
||
},
|
||
}
|
||
}
|
||
|
||
fn handle_text(&mut self, info: &NodeAndStyleInfo<'dom>, text: Cow<'dom, str>) {
|
||
if text.is_empty() {
|
||
return;
|
||
}
|
||
|
||
// If we are building an anonymous table ie this text directly followed internal
|
||
// table elements that did not have a `<table>` ancestor, then we forward all
|
||
// whitespace to the table builder.
|
||
if !self.anonymous_table_content.is_empty() && text.chars().all(char_is_whitespace) {
|
||
self.anonymous_table_content
|
||
.push(AnonymousTableContent::Text(info.clone(), text));
|
||
return;
|
||
} else {
|
||
self.finish_anonymous_table_if_needed();
|
||
}
|
||
|
||
let container_info = self.info;
|
||
let context = self.context;
|
||
let builder = self.ensure_inline_formatting_context_builder();
|
||
|
||
// ::first-letter is an eager pseudo element and should not be nested
|
||
if let Some(pseudo_info) = (container_info.pseudo_element_chain().is_empty())
|
||
.then(|| container_info.with_pseudo_element(context, PseudoElement::FirstLetter))
|
||
.flatten()
|
||
.filter(|_| {
|
||
builder
|
||
.text_segments
|
||
.iter()
|
||
.flat_map(|seg| seg.chars())
|
||
.all(|c| c.is_separator_space())
|
||
})
|
||
{
|
||
let first_letter_range = first_letter_range(&text[..]);
|
||
|
||
// The first letter range may be some value larger than zero when
|
||
// there are preceding spaces.
|
||
if first_letter_range.start != 0 {
|
||
builder.push_text(Cow::Borrowed(&text[0..first_letter_range.start]), info);
|
||
}
|
||
|
||
builder.start_inline_box(
|
||
|| ArcRefCell::new(InlineBox::new(&pseudo_info, context)),
|
||
None,
|
||
);
|
||
let first_letter_text = Cow::Borrowed(&text[first_letter_range.clone()]);
|
||
builder.push_text(first_letter_text, &pseudo_info);
|
||
builder.end_inline_box();
|
||
|
||
builder.push_text(Cow::Borrowed(&text[first_letter_range.end..]), info);
|
||
} else {
|
||
builder.push_text(text, info);
|
||
}
|
||
}
|
||
|
||
fn enter_display_contents(&mut self, styles: SharedInlineStyles) {
|
||
self.display_contents_shared_styles.push(styles.clone());
|
||
if let Some(builder) = self.inline_formatting_context_builder.as_mut() {
|
||
builder.enter_display_contents(styles);
|
||
}
|
||
}
|
||
|
||
fn leave_display_contents(&mut self) {
|
||
self.display_contents_shared_styles.pop();
|
||
if let Some(builder) = self.inline_formatting_context_builder.as_mut() {
|
||
builder.leave_display_contents();
|
||
}
|
||
}
|
||
}
|
||
|
||
impl<'dom> BlockContainerBuilder<'dom, '_> {
|
||
fn handle_list_item_marker_inside(
|
||
&mut self,
|
||
marker_info: &NodeAndStyleInfo<'dom>,
|
||
contents: Vec<crate::dom_traversal::PseudoElementContentItem>,
|
||
) {
|
||
let box_slot = marker_info.node.box_slot();
|
||
self.handle_inline_level_element(
|
||
marker_info,
|
||
DisplayInside::Flow {
|
||
is_list_item: false,
|
||
},
|
||
Contents::for_pseudo_element(contents),
|
||
box_slot,
|
||
);
|
||
}
|
||
|
||
fn handle_list_item_marker_outside(
|
||
&mut self,
|
||
marker_info: &NodeAndStyleInfo<'dom>,
|
||
contents: Vec<crate::dom_traversal::PseudoElementContentItem>,
|
||
list_item_style: Arc<ComputedValues>,
|
||
) {
|
||
let box_slot = marker_info.node.box_slot();
|
||
self.block_level_boxes.push(BlockLevelJob {
|
||
info: marker_info.clone(),
|
||
box_slot,
|
||
kind: BlockLevelCreator::OutsideMarker {
|
||
contents,
|
||
list_item_style,
|
||
},
|
||
propagated_data: self.propagated_data,
|
||
});
|
||
}
|
||
|
||
fn handle_inline_level_element(
|
||
&mut self,
|
||
info: &NodeAndStyleInfo<'dom>,
|
||
display_inside: DisplayInside,
|
||
contents: Contents,
|
||
box_slot: BoxSlot<'dom>,
|
||
) {
|
||
let context = self.context;
|
||
let old_layout_box = box_slot.take_layout_box();
|
||
let (is_list_item, non_replaced_contents) = match (display_inside, contents) {
|
||
(
|
||
DisplayInside::Flow { is_list_item },
|
||
Contents::NonReplaced(non_replaced_contents),
|
||
) => (is_list_item, non_replaced_contents),
|
||
(_, contents) => {
|
||
// If this inline element is an atomic, handle it and return.
|
||
let propagated_data = self.propagated_data;
|
||
|
||
let construction_callback = || {
|
||
ArcRefCell::new(IndependentFormattingContext::construct(
|
||
context,
|
||
info,
|
||
display_inside,
|
||
contents,
|
||
propagated_data,
|
||
))
|
||
};
|
||
|
||
let atomic = self
|
||
.ensure_inline_formatting_context_builder()
|
||
.push_atomic(construction_callback, old_layout_box);
|
||
box_slot.set(LayoutBox::InlineLevel(atomic));
|
||
return;
|
||
},
|
||
};
|
||
|
||
// Otherwise, this is just a normal inline box. Whatever happened before, all we need to do
|
||
// before recurring is to remember this ongoing inline level box.
|
||
let inline_builder = self.ensure_inline_formatting_context_builder();
|
||
inline_builder.start_inline_box(
|
||
|| ArcRefCell::new(InlineBox::new(info, context)),
|
||
old_layout_box,
|
||
);
|
||
box_slot.set(LayoutBox::InlineLevel(
|
||
inline_builder.inline_items.last().unwrap().clone(),
|
||
));
|
||
|
||
if is_list_item {
|
||
if let Some((marker_info, marker_contents)) =
|
||
crate::lists::make_marker(self.context, info)
|
||
{
|
||
// Ignore `list-style-position` here:
|
||
// “If the list item is an inline box: this value is equivalent to `inside`.”
|
||
// https://drafts.csswg.org/css-lists/#list-style-position-outside
|
||
self.handle_list_item_marker_inside(&marker_info, marker_contents)
|
||
}
|
||
}
|
||
|
||
// `unwrap` doesn’t panic here because `is_replaced` returned `false`.
|
||
non_replaced_contents.traverse(self.context, info, self);
|
||
|
||
self.finish_anonymous_table_if_needed();
|
||
|
||
self.inline_formatting_context_builder
|
||
.as_mut()
|
||
.expect("Should be building an InlineFormattingContext")
|
||
.end_inline_box();
|
||
}
|
||
|
||
fn handle_block_level_element(
|
||
&mut self,
|
||
info: &NodeAndStyleInfo<'dom>,
|
||
display_inside: DisplayInside,
|
||
contents: Contents,
|
||
box_slot: BoxSlot<'dom>,
|
||
) {
|
||
let propagated_data = self.propagated_data;
|
||
let kind = BlockLevelCreator::new_for_inflow_block_level_element(
|
||
info,
|
||
display_inside,
|
||
contents,
|
||
propagated_data,
|
||
);
|
||
let job = BlockLevelJob {
|
||
info: info.clone(),
|
||
box_slot,
|
||
kind,
|
||
propagated_data,
|
||
};
|
||
if let Some(builder) = self.inline_formatting_context_builder.as_mut() {
|
||
if builder.currently_processing_inline_box() {
|
||
builder.push_block_level_box(job.finish(self.context), self.info, self.context);
|
||
return;
|
||
}
|
||
if let Some(context) = self.finish_ongoing_inline_formatting_context() {
|
||
self.push_block_level_job_for_inline_formatting_context(context);
|
||
}
|
||
}
|
||
self.block_level_boxes.push(job);
|
||
|
||
// Any block also counts as the first line for the purposes of text indent. Even if
|
||
// they don't actually indent.
|
||
self.have_already_seen_first_line_for_text_indent = true;
|
||
}
|
||
|
||
fn handle_absolutely_positioned_element(
|
||
&mut self,
|
||
info: &NodeAndStyleInfo<'dom>,
|
||
display_inside: DisplayInside,
|
||
contents: Contents,
|
||
box_slot: BoxSlot<'dom>,
|
||
) {
|
||
// If the original display was inline-level, then we need an inline formatting context
|
||
// in order to compute the static position correctly.
|
||
// If it was block-level, we don't want to break an existing inline formatting context,
|
||
// so push it there (`LineItemLayout::layout_absolute` can handle this well). But if
|
||
// there is no inline formatting context, then we can avoid creating one.
|
||
let needs_inline_builder =
|
||
info.style.get_box().original_display.outside() == StyloDisplayOutside::Inline;
|
||
if needs_inline_builder {
|
||
self.ensure_inline_formatting_context_builder();
|
||
}
|
||
let inline_builder = self
|
||
.inline_formatting_context_builder
|
||
.as_mut()
|
||
.filter(|builder| needs_inline_builder || !builder.is_empty);
|
||
if let Some(inline_builder) = inline_builder {
|
||
let constructor = || {
|
||
ArcRefCell::new(AbsolutelyPositionedBox::construct(
|
||
self.context,
|
||
info,
|
||
display_inside,
|
||
contents,
|
||
))
|
||
};
|
||
let old_layout_box = box_slot.take_layout_box();
|
||
let inline_level_box =
|
||
inline_builder.push_absolutely_positioned_box(constructor, old_layout_box);
|
||
box_slot.set(LayoutBox::InlineLevel(inline_level_box));
|
||
return;
|
||
}
|
||
|
||
let kind = BlockLevelCreator::OutOfFlowAbsolutelyPositionedBox {
|
||
contents,
|
||
display_inside,
|
||
};
|
||
self.block_level_boxes.push(BlockLevelJob {
|
||
info: info.clone(),
|
||
box_slot,
|
||
kind,
|
||
propagated_data: self.propagated_data,
|
||
});
|
||
}
|
||
|
||
fn handle_float_element(
|
||
&mut self,
|
||
info: &NodeAndStyleInfo<'dom>,
|
||
display_inside: DisplayInside,
|
||
contents: Contents,
|
||
box_slot: BoxSlot<'dom>,
|
||
) {
|
||
if let Some(builder) = self.inline_formatting_context_builder.as_mut() {
|
||
if !builder.is_empty {
|
||
let constructor = || {
|
||
ArcRefCell::new(FloatBox::construct(
|
||
self.context,
|
||
info,
|
||
display_inside,
|
||
contents,
|
||
self.propagated_data,
|
||
))
|
||
};
|
||
let old_layout_box = box_slot.take_layout_box();
|
||
let inline_level_box = builder.push_float_box(constructor, old_layout_box);
|
||
box_slot.set(LayoutBox::InlineLevel(inline_level_box));
|
||
return;
|
||
}
|
||
}
|
||
|
||
let kind = BlockLevelCreator::OutOfFlowFloatBox {
|
||
contents,
|
||
display_inside,
|
||
};
|
||
self.block_level_boxes.push(BlockLevelJob {
|
||
info: info.clone(),
|
||
box_slot,
|
||
kind,
|
||
propagated_data: self.propagated_data,
|
||
});
|
||
}
|
||
|
||
fn push_block_level_job_for_inline_formatting_context(
|
||
&mut self,
|
||
inline_formatting_context: InlineFormattingContext,
|
||
) {
|
||
let layout_context = self.context;
|
||
let anonymous_info = self
|
||
.anonymous_box_info
|
||
.get_or_insert_with(|| {
|
||
self.info
|
||
.with_pseudo_element(layout_context, PseudoElement::ServoAnonymousBox)
|
||
.expect("Should never fail to create anonymous box")
|
||
})
|
||
.clone();
|
||
|
||
let box_slot = anonymous_info.node.box_slot();
|
||
self.block_level_boxes.push(BlockLevelJob {
|
||
info: anonymous_info,
|
||
box_slot,
|
||
kind: BlockLevelCreator::SameFormattingContextBlock(
|
||
IntermediateBlockContainer::InlineFormattingContext(
|
||
BlockContainer::InlineFormattingContext(inline_formatting_context),
|
||
),
|
||
),
|
||
propagated_data: self.propagated_data,
|
||
});
|
||
|
||
self.have_already_seen_first_line_for_text_indent = true;
|
||
}
|
||
}
|
||
|
||
impl BlockLevelJob<'_> {
|
||
fn finish(self, context: &LayoutContext) -> ArcRefCell<BlockLevelBox> {
|
||
let info = &self.info;
|
||
|
||
// If this `BlockLevelBox` exists, it has been laid out before and is
|
||
// reusable.
|
||
if let Some(block_level_box) = match &*self.box_slot.slot.borrow() {
|
||
Some(LayoutBox::BlockLevel(block_level_box)) => Some(block_level_box.clone()),
|
||
_ => None,
|
||
} {
|
||
return block_level_box;
|
||
}
|
||
|
||
let block_level_box = match self.kind {
|
||
BlockLevelCreator::SameFormattingContextBlock(intermediate_block_container) => {
|
||
let contents = intermediate_block_container.finish(context, info);
|
||
let contains_floats = contents.contains_floats();
|
||
ArcRefCell::new(BlockLevelBox::SameFormattingContextBlock {
|
||
base: LayoutBoxBase::new(info.into(), info.style.clone()),
|
||
contents,
|
||
contains_floats,
|
||
})
|
||
},
|
||
BlockLevelCreator::Independent {
|
||
display_inside,
|
||
contents,
|
||
} => {
|
||
let context = IndependentFormattingContext::construct(
|
||
context,
|
||
info,
|
||
display_inside,
|
||
contents,
|
||
self.propagated_data,
|
||
);
|
||
ArcRefCell::new(BlockLevelBox::Independent(context))
|
||
},
|
||
BlockLevelCreator::OutOfFlowAbsolutelyPositionedBox {
|
||
display_inside,
|
||
contents,
|
||
} => ArcRefCell::new(BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(
|
||
ArcRefCell::new(AbsolutelyPositionedBox::construct(
|
||
context,
|
||
info,
|
||
display_inside,
|
||
contents,
|
||
)),
|
||
)),
|
||
BlockLevelCreator::OutOfFlowFloatBox {
|
||
display_inside,
|
||
contents,
|
||
} => ArcRefCell::new(BlockLevelBox::OutOfFlowFloatBox(FloatBox::construct(
|
||
context,
|
||
info,
|
||
display_inside,
|
||
contents,
|
||
self.propagated_data,
|
||
))),
|
||
BlockLevelCreator::OutsideMarker {
|
||
contents,
|
||
list_item_style,
|
||
} => {
|
||
let contents = NonReplacedContents::OfPseudoElement(contents);
|
||
let block_container = BlockContainer::construct(
|
||
context,
|
||
info,
|
||
contents,
|
||
self.propagated_data,
|
||
false, /* is_list_item */
|
||
);
|
||
// An outside ::marker must establish a BFC, and can't contain floats.
|
||
let block_formatting_context = BlockFormattingContext {
|
||
contents: block_container,
|
||
contains_floats: false,
|
||
};
|
||
ArcRefCell::new(BlockLevelBox::OutsideMarker(OutsideMarker {
|
||
context: IndependentFormattingContext::new(
|
||
LayoutBoxBase::new(info.into(), info.style.clone()),
|
||
IndependentFormattingContextContents::Flow(block_formatting_context),
|
||
self.propagated_data,
|
||
),
|
||
list_item_style,
|
||
}))
|
||
},
|
||
BlockLevelCreator::AnonymousTable { table_block } => table_block,
|
||
};
|
||
self.box_slot
|
||
.set(LayoutBox::BlockLevel(block_level_box.clone()));
|
||
block_level_box
|
||
}
|
||
}
|
||
|
||
impl IntermediateBlockContainer {
|
||
fn finish(self, context: &LayoutContext, info: &NodeAndStyleInfo<'_>) -> BlockContainer {
|
||
match self {
|
||
IntermediateBlockContainer::Deferred {
|
||
contents,
|
||
propagated_data,
|
||
is_list_item,
|
||
} => BlockContainer::construct(context, info, contents, propagated_data, is_list_item),
|
||
IntermediateBlockContainer::InlineFormattingContext(block_container) => block_container,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn assert_first_letter_eq(text: &str, expected: &str) {
|
||
let range = first_letter_range(text);
|
||
assert_eq!(&text[range], expected);
|
||
}
|
||
|
||
#[test]
|
||
fn test_first_letter_range() {
|
||
// All spaces
|
||
assert_first_letter_eq("", "");
|
||
assert_first_letter_eq(" ", "");
|
||
|
||
// Spaces and punctuation only
|
||
assert_first_letter_eq("(", "");
|
||
assert_first_letter_eq(" (", "");
|
||
assert_first_letter_eq("( ", "");
|
||
assert_first_letter_eq("()", "");
|
||
|
||
// Invalid chars
|
||
assert_first_letter_eq("\u{0903}", "");
|
||
|
||
// First letter only
|
||
assert_first_letter_eq("A", "A");
|
||
assert_first_letter_eq(" A", "A");
|
||
assert_first_letter_eq("A ", "A");
|
||
assert_first_letter_eq(" A ", "A");
|
||
|
||
// Word
|
||
assert_first_letter_eq("App", "A");
|
||
assert_first_letter_eq(" App", "A");
|
||
assert_first_letter_eq("App ", "A");
|
||
|
||
// Preceding punctuation(s), intervening spaces and first letter
|
||
assert_first_letter_eq(r#""A"#, r#""A"#);
|
||
assert_first_letter_eq(r#" "A"#, r#""A"#);
|
||
assert_first_letter_eq(r#""A "#, r#""A"#);
|
||
assert_first_letter_eq(r#"" A"#, r#"" A"#);
|
||
assert_first_letter_eq(r#" "A "#, r#""A"#);
|
||
assert_first_letter_eq(r#"("A"#, r#"("A"#);
|
||
assert_first_letter_eq(r#" ("A"#, r#"("A"#);
|
||
assert_first_letter_eq(r#"( "A"#, r#"( "A"#);
|
||
assert_first_letter_eq(r#"[ ( "A"#, r#"[ ( "A"#);
|
||
|
||
// First letter and succeeding punctuation(s)
|
||
// TODO: modify test cases when intervening spaces in succeeding puntuations is supported
|
||
assert_first_letter_eq(r#"A""#, r#"A""#);
|
||
assert_first_letter_eq(r#"A" "#, r#"A""#);
|
||
assert_first_letter_eq(r#"A)]"#, r#"A)]"#);
|
||
assert_first_letter_eq(r#"A" )]"#, r#"A""#);
|
||
assert_first_letter_eq(r#"A)] >"#, r#"A)]"#);
|
||
|
||
// All
|
||
assert_first_letter_eq(r#" ("A" )]"#, r#"("A""#);
|
||
assert_first_letter_eq(r#" ("A")] >"#, r#"("A")]"#);
|
||
|
||
// Non ASCII chars
|
||
assert_first_letter_eq("一", "一");
|
||
assert_first_letter_eq(" 一 ", "一");
|
||
assert_first_letter_eq("一二三", "一");
|
||
assert_first_letter_eq(" 一二三 ", "一");
|
||
assert_first_letter_eq("(一二三)", "(一");
|
||
assert_first_letter_eq(" (一二三) ", "(一");
|
||
assert_first_letter_eq("((一", "((一");
|
||
assert_first_letter_eq(" ( (一", "( (一");
|
||
assert_first_letter_eq("一)", "一)");
|
||
assert_first_letter_eq("一))", "一))");
|
||
assert_first_letter_eq("一) )", "一)");
|
||
}
|
||
}
|