diff --git a/components/layout_2020/flexbox/layout.rs b/components/layout_2020/flexbox/layout.rs index 11f8bb2826a..c19a4e509c9 100644 --- a/components/layout_2020/flexbox/layout.rs +++ b/components/layout_2020/flexbox/layout.rs @@ -25,7 +25,7 @@ use super::geom::{ use super::{FlexContainer, FlexLevelBox}; use crate::cell::ArcRefCell; use crate::context::LayoutContext; -use crate::formatting_contexts::{IndependentFormattingContext, IndependentLayout}; +use crate::formatting_contexts::{Baselines, IndependentFormattingContext, IndependentLayout}; use crate::fragment_tree::{BoxFragment, CollapsedBlockMargins, Fragment}; use crate::geom::{AuOrAuto, LengthOrAuto, LogicalRect, LogicalSides, LogicalVec2}; use crate::positioned::{AbsolutelyPositionedBox, PositioningContext, PositioningContextLength}; @@ -410,7 +410,7 @@ impl FlexContainer { IndependentLayout { fragments, content_block_size: content_block_size.into(), - last_inflow_baseline_offset: None, + baselines: Baselines::default(), } } } @@ -848,6 +848,7 @@ impl FlexLine<'_> { let margin = flex_context.sides_to_flow_relative(*margin); let collapsed_margin = CollapsedBlockMargins::from_margin(&margin); ( + // TODO: We should likely propagate baselines from `display: flex`. BoxFragment::new( item.box_.base_fragment_info(), item.box_.style().clone(), @@ -857,8 +858,6 @@ impl FlexLine<'_> { flex_context.sides_to_flow_relative(item.border.map(|t| (*t).into())), margin, None, /* clearance */ - // TODO: We should likely propagate baselines from `display: flex`. - None, /* last_inflow_baseline_offset */ collapsed_margin, ), item_result.positioning_context, diff --git a/components/layout_2020/flow/float.rs b/components/layout_2020/flow/float.rs index 62be9038e03..95c64c81b30 100644 --- a/components/layout_2020/flow/float.rs +++ b/components/layout_2020/flow/float.rs @@ -982,7 +982,6 @@ impl FloatBox { // Clearance is handled internally by the float placement logic, so there's no need // to store it explicitly in the fragment. None, // clearance - None, // last_inflow_baseline_offset CollapsedBlockMargins::zero(), ) }, diff --git a/components/layout_2020/flow/inline.rs b/components/layout_2020/flow/inline.rs index b4585bd7efe..6c36f009711 100644 --- a/components/layout_2020/flow/inline.rs +++ b/components/layout_2020/flow/inline.rs @@ -34,7 +34,7 @@ use crate::cell::ArcRefCell; use crate::context::LayoutContext; use crate::flow::float::{FloatBox, SequentialLayoutState}; use crate::flow::FlowLayout; -use crate::formatting_contexts::IndependentFormattingContext; +use crate::formatting_contexts::{Baselines, IndependentFormattingContext}; use crate::fragment_tree::{ AnonymousFragment, BaseFragmentInfo, BoxFragment, CollapsedBlockMargins, CollapsedMargin, Fragment, @@ -246,9 +246,9 @@ impl LineBlockSizes { let height_from_ascent_and_descent = self .baseline_relative_size_for_line_height .as_ref() - .map(|size| Length::from((size.ascent + size.descent).abs())) - .unwrap_or_else(Length::zero); - self.line_height.max(height_from_ascent_and_descent) + .map(|size| (size.ascent + size.descent).abs()) + .unwrap_or_else(Au::zero); + self.line_height.max(height_from_ascent_and_descent.into()) } fn max(&self, other: &LineBlockSizes) -> LineBlockSizes { @@ -286,17 +286,16 @@ impl LineBlockSizes { /// > to minimize the line box height. If such boxes are tall enough, there are multiple /// > solutions and CSS 2 does not define the position of the line box's baseline (i.e., /// > the position of the strut, see below). - fn find_baseline_offset(&self) -> Length { + fn find_baseline_offset(&self) -> Au { match self.baseline_relative_size_for_line_height.as_ref() { - Some(size) => size.ascent.into(), + Some(size) => size.ascent, None => { // This is the case mentinoned above where there are multiple solutions. // This code is putting the baseline roughly in the middle of the line. - let leading = self.resolve() - + let leading = Au::from(self.resolve()) - (self.size_for_baseline_positioning.ascent + - self.size_for_baseline_positioning.descent) - .into(); - leading.scale_by(0.5) + self.size_for_baseline_positioning.ascent.into() + self.size_for_baseline_positioning.descent); + leading.scale_by(0.5) + self.size_for_baseline_positioning.ascent }, } } @@ -573,10 +572,10 @@ pub(super) struct InlineFormattingContextState<'a, 'b> { /// common ancestor is used. white_space: WhiteSpace, - /// The offset of the last baseline in the inline formatting context that we + /// The offset of the first and last baselines in the inline formatting context that we /// are laying out. This is used to propagate baselines to the ancestors of - /// `display: inline-block` elements. - last_baseline_offset: Option, + /// `display: inline-block` elements and table content. + baselines: Baselines, } impl<'a, 'b> InlineFormattingContextState<'a, 'b> { @@ -760,7 +759,10 @@ impl<'a, 'b> InlineFormattingContextState<'a, 'b> { return; } - self.last_baseline_offset = Some(baseline_offset + block_start_position.into()); + let baseline = baseline_offset + block_start_position; + self.baselines.first.get_or_insert(baseline); + self.baselines.last = Some(baseline); + let line_rect = LogicalRect { // The inline part of this start offset was taken into account when determining // the inline start of the line in `calculate_inline_start_for_current_line` so @@ -1453,7 +1455,7 @@ impl InlineFormattingContext { prevent_soft_wrap_opportunity_before_next_atomic: false, had_inflow_content: false, white_space: containing_block.style.get_inherited_text().white_space, - last_baseline_offset: None, + baselines: Baselines::default(), }; // FIXME(pcwalton): This assumes that margins never collapse through inline formatting @@ -1506,7 +1508,7 @@ impl InlineFormattingContext { fragments: ifc.fragments, content_block_size, collapsible_margins_in_children, - last_inflow_baseline_offset: ifc.last_baseline_offset, + baselines: ifc.baselines, } } @@ -1791,7 +1793,6 @@ impl IndependentFormattingContext { pbm.border, margin, None, /* clearance */ - None, /* last_inflow_baseline_offset */ CollapsedBlockMargins::zero(), ) }, @@ -1873,11 +1874,9 @@ impl IndependentFormattingContext { pbm.border, margin, None, - independent_layout - .last_inflow_baseline_offset - .map(|t| t.into()), CollapsedBlockMargins::zero(), ) + .with_baselines(independent_layout.baselines) }, }; @@ -1891,10 +1890,10 @@ impl IndependentFormattingContext { let size = &pbm_sums.sum() + &fragment.content_rect.size; let baseline_offset = fragment - .last_baseline_offset - .map(|baseline_offset| pbm_sums.block_start + baseline_offset) - .unwrap_or(size.block); - let baseline_offset = Au::from_f32_px(baseline_offset.px()); + .baselines + .last + .map(|baseline| Au::from(pbm_sums.block_start) + baseline) + .unwrap_or(size.block.into()); let (block_sizes, baseline_offset_in_parent) = self.get_block_sizes_and_baseline_offset(ifc, size.block, baseline_offset); diff --git a/components/layout_2020/flow/line.rs b/components/layout_2020/flow/line.rs index 146f3388120..b36b9fdb4c0 100644 --- a/components/layout_2020/flow/line.rs +++ b/components/layout_2020/flow/line.rs @@ -40,7 +40,7 @@ pub(super) struct LineMetrics { pub block_size: Length, /// The block offset of this line's baseline from [`Self:block_offset`]. - pub baseline_block_offset: Length, + pub baseline_block_offset: Au, } /// State used when laying out the [`LineItem`]s collected for the line currently being @@ -54,7 +54,7 @@ pub(super) struct LineItemLayoutState<'a> { /// The block offset of the parent's baseline relative to the block start of the line. This /// is often the same as [`Self::block_offset_of_parent`], but can be different for the root /// element. - pub baseline_offset: Length, + pub baseline_offset: Au, pub ifc_containing_block: &'a ContainingBlock<'a>, pub positioning_context: &'a mut PositioningContext, @@ -255,7 +255,7 @@ impl TextRunLineItem { // fallback fonts that use baseline relatve alignment, it might be different. let mut start_corner = &LogicalVec2 { inline: state.inline_position, - block: state.baseline_offset - self.font_metrics.ascent.into(), + block: (state.baseline_offset - self.font_metrics.ascent).into(), } - &state.parent_offset; if !is_baseline_relative( self.parent_style @@ -350,7 +350,7 @@ impl InlineBoxLineItem { inline_position: state.inline_position, parent_offset: LogicalVec2 { inline: state.inline_position, - block: block_start_offset, + block: block_start_offset.into(), }, ifc_containing_block: state.ifc_containing_block, positioning_context: nested_positioning_context, @@ -388,7 +388,7 @@ impl InlineBoxLineItem { let mut content_rect = LogicalRect { start_corner: LogicalVec2 { inline: state.inline_position, - block: block_start_offset, + block: block_start_offset.into(), }, size: LogicalVec2 { inline: nested_state.inline_position - state.inline_position, @@ -405,6 +405,9 @@ impl InlineBoxLineItem { content_rect.start_corner += &relative_adjustement(&style, state.ifc_containing_block); } + // NB: There is no need to set a baseline offset for this BoxFragment, because the + // baselines of this InlineFormattingContext is what will propagate to `display: + // inline-block` ancestors. let mut fragment = BoxFragment::new( self.base_fragment_info, self.style.clone(), @@ -414,10 +417,6 @@ impl InlineBoxLineItem { border, margin, None, /* clearance */ - // There is no need to set a baseline offset for this BoxFragment, because - // the last baseline of this InlineFormattingContext is what will propagate - // to `display: inline-block` ancestors. - None, /* last_inflow_baseline_offset */ CollapsedBlockMargins::zero(), ); @@ -446,23 +445,19 @@ impl InlineBoxLineItem { /// Given our font metrics, calculate the space above the baseline we need for our content. /// Note that this space does not include space for any content in child inline boxes, as /// they are not included in our content rect. - fn calculate_space_above_baseline(&self) -> Length { + fn calculate_space_above_baseline(&self) -> Au { let (ascent, descent, line_gap) = ( self.font_metrics.ascent, self.font_metrics.descent, self.font_metrics.line_gap, ); let leading = line_gap - (ascent + descent); - (leading.scale_by(0.5) + ascent).into() + leading.scale_by(0.5) + ascent } /// Given the state for a line item layout and the space above the baseline for this inline /// box, find the block start position relative to the line block start position. - fn calculate_block_start( - &self, - state: &LineItemLayoutState, - space_above_baseline: Length, - ) -> Length { + fn calculate_block_start(&self, state: &LineItemLayoutState, space_above_baseline: Au) -> Au { let vertical_align = self.style.effective_vertical_align_for_inline_layout(); let line_gap = self.font_metrics.line_gap; @@ -470,16 +465,16 @@ impl InlineBoxLineItem { // baseline, so we need to make it relative to the line block start. match vertical_align { GenericVerticalAlign::Keyword(VerticalAlignKeyword::Top) => { - let line_height = line_height(&self.style, &self.font_metrics); - (line_height - line_gap.into()).scale_by(0.5) + let line_height: Au = line_height(&self.style, &self.font_metrics).into(); + (line_height - line_gap).scale_by(0.5) }, GenericVerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => { - let line_height = line_height(&self.style, &self.font_metrics); - let half_leading = (line_height - line_gap.into()).scale_by(0.5); - state.line_metrics.block_size - line_height + half_leading + let line_height: Au = line_height(&self.style, &self.font_metrics).into(); + let half_leading = (line_height - line_gap).scale_by(0.5); + Au::from(state.line_metrics.block_size) - line_height + half_leading }, _ => { - state.line_metrics.baseline_block_offset + Length::from(self.baseline_offset) - + state.line_metrics.baseline_block_offset + self.baseline_offset - space_above_baseline }, } @@ -542,9 +537,8 @@ impl AtomicLineItem { // This covers all baseline-relative vertical alignment. _ => { - let baseline = line_metrics.baseline_block_offset + - Length::from(self.baseline_offset_in_parent); - baseline - Length::from(self.baseline_offset_in_item) + let baseline = line_metrics.baseline_block_offset + self.baseline_offset_in_parent; + Length::from(baseline - self.baseline_offset_in_item) }, } } diff --git a/components/layout_2020/flow/mod.rs b/components/layout_2020/flow/mod.rs index 0ffac153258..d56aed65571 100644 --- a/components/layout_2020/flow/mod.rs +++ b/components/layout_2020/flow/mod.rs @@ -22,7 +22,7 @@ use crate::flow::float::{ }; use crate::flow::inline::InlineFormattingContext; use crate::formatting_contexts::{ - IndependentFormattingContext, IndependentLayout, NonReplacedFormattingContext, + Baselines, IndependentFormattingContext, IndependentLayout, NonReplacedFormattingContext, }; use crate::fragment_tree::{ BaseFragmentInfo, BoxFragment, CollapsedBlockMargins, CollapsedMargin, Fragment, @@ -198,10 +198,10 @@ struct FlowLayout { pub fragments: Vec, pub content_block_size: Length, pub collapsible_margins_in_children: CollapsedBlockMargins, - /// The offset of the last inflow baseline in this layout in the content area, if - /// there was one. This is used to propagate inflow baselines to the ancestors - /// of `display: inline-block` elements. - pub last_inflow_baseline_offset: Option, + /// The offset of the baselines in this layout in the content area, if there were some. This is + /// used to propagate inflow baselines to the ancestors of `display: inline-block` elements + /// and table content. + pub baselines: Baselines, } #[derive(Clone, Copy)] @@ -248,7 +248,7 @@ impl BlockFormattingContext { flow_layout.collapsible_margins_in_children.end.solve() + clearance.unwrap_or_else(Au::zero).into()) .into(), - last_inflow_baseline_offset: flow_layout.last_inflow_baseline_offset.map(|t| t.into()), + baselines: flow_layout.baselines, } } } @@ -476,13 +476,12 @@ fn layout_block_level_children_in_parallel( }) .collect(); - let (content_block_size, collapsible_margins_in_children, baseline_offset) = - placement_state.finish(); + let (content_block_size, collapsible_margins_in_children, baselines) = placement_state.finish(); FlowLayout { fragments, content_block_size, collapsible_margins_in_children, - last_inflow_baseline_offset: baseline_offset, + baselines, } } @@ -524,13 +523,12 @@ fn layout_block_level_children_sequentially( }) .collect(); - let (content_block_size, collapsible_margins_in_children, baseline_offset) = - placement_state.finish(); + let (content_block_size, collapsible_margins_in_children, baselines) = placement_state.finish(); FlowLayout { fragments, content_block_size, collapsible_margins_in_children, - last_inflow_baseline_offset: baseline_offset, + baselines, } } @@ -818,9 +816,9 @@ fn layout_in_flow_non_replaced_block_level_same_formatting_context( pbm.border, margin, clearance.map(|t| t.into()), - flow_layout.last_inflow_baseline_offset, block_margins_collapsed_with_children, ) + .with_baselines(flow_layout.baselines) } impl NonReplacedFormattingContext { @@ -893,9 +891,9 @@ impl NonReplacedFormattingContext { pbm.border, margin, None, /* clearance */ - layout.last_inflow_baseline_offset.map(|t| t.into()), block_margins_collapsed_with_children, ) + .with_baselines(layout.baselines) } /// Lay out a normal in flow non-replaced block that establishes an independent @@ -1114,9 +1112,9 @@ impl NonReplacedFormattingContext { pbm.border, margin, clearance, - layout.last_inflow_baseline_offset.map(|t| t.into()), block_margins_collapsed_with_children, ) + .with_baselines(layout.baselines) } } @@ -1211,7 +1209,6 @@ fn layout_in_flow_replaced_block_level<'a>( pbm.border, margin, clearance, - None, /* last_inflow_base_offset */ block_margins_collapsed_with_children, ) } @@ -1389,7 +1386,7 @@ struct PlacementState { start_margin: CollapsedMargin, current_margin: CollapsedMargin, current_block_direction_position: Length, - last_inflow_baseline_offset: Option, + inflow_baselines: Baselines, } impl PlacementState { @@ -1403,7 +1400,7 @@ impl PlacementState { start_margin: CollapsedMargin::zero(), current_margin: CollapsedMargin::zero(), current_block_direction_position: Length::zero(), - last_inflow_baseline_offset: None, + inflow_baselines: Baselines::default(), } } @@ -1414,14 +1411,15 @@ impl PlacementState { ) { self.place_fragment(fragment, sequential_layout_state); - match fragment { - Fragment::Box(box_fragment) => { - if let Some(baseline) = box_fragment.last_baseline_offset { - self.last_inflow_baseline_offset = - Some(baseline + box_fragment.content_rect.start_corner.block); - } - }, - _ => {}, + if let Fragment::Box(box_fragment) = fragment { + let box_block_offset = box_fragment.content_rect.start_corner.block.into(); + match (self.inflow_baselines.first, box_fragment.baselines.first) { + (None, Some(first)) => self.inflow_baselines.first = Some(first + box_block_offset), + _ => {}, + } + if let Some(last) = box_fragment.baselines.last { + self.inflow_baselines.last = Some(last + box_block_offset); + } } } @@ -1511,7 +1509,7 @@ impl PlacementState { } } - fn finish(mut self) -> (Length, CollapsedBlockMargins, Option) { + fn finish(mut self) -> (Length, CollapsedBlockMargins, Baselines) { if !self.last_in_flow_margin_collapses_with_parent_end_margin { self.current_block_direction_position += self.current_margin.solve(); self.current_margin = CollapsedMargin::zero(); @@ -1523,7 +1521,7 @@ impl PlacementState { start: self.start_margin, end: self.current_margin, }, - self.last_inflow_baseline_offset, + self.inflow_baselines, ) } } diff --git a/components/layout_2020/formatting_contexts.rs b/components/layout_2020/formatting_contexts.rs index aa720ee5d9d..37e72234b7e 100644 --- a/components/layout_2020/formatting_contexts.rs +++ b/components/layout_2020/formatting_contexts.rs @@ -59,6 +59,14 @@ pub(crate) enum NonReplacedFormattingContextContents { // Other layout modes go here } +/// The baselines of a layout or a [`BoxFragment`]. Some layout uses the first and some layout uses +/// the last. +#[derive(Default, Serialize)] +pub(crate) struct Baselines { + pub first: Option, + pub last: Option, +} + pub(crate) struct IndependentLayout { pub fragments: Vec, @@ -68,7 +76,7 @@ pub(crate) struct IndependentLayout { /// The offset of the last inflow baseline of this layout in the content area, if /// there was one. This is used to propagate baselines to the ancestors of `display: /// inline-block`. - pub last_inflow_baseline_offset: Option, + pub baselines: Baselines, } impl IndependentFormattingContext { diff --git a/components/layout_2020/fragment_tree/box_fragment.rs b/components/layout_2020/fragment_tree/box_fragment.rs index f689c646280..fe83b334ae8 100644 --- a/components/layout_2020/fragment_tree/box_fragment.rs +++ b/components/layout_2020/fragment_tree/box_fragment.rs @@ -13,6 +13,7 @@ use style::Zero; use super::{BaseFragment, BaseFragmentInfo, CollapsedBlockMargins, Fragment}; use crate::cell::ArcRefCell; +use crate::formatting_contexts::Baselines; use crate::geom::{ LengthOrAuto, LogicalRect, LogicalSides, PhysicalPoint, PhysicalRect, PhysicalSides, PhysicalSize, @@ -44,10 +45,10 @@ pub(crate) struct BoxFragment { /// pub clearance: Option, - /// When this box contains an inline formatting context, this tracks the baseline - /// offset of the last inflow line. This offset is used to propagate baselines to - /// ancestors of `display: inline-block` ancestors. - pub last_baseline_offset: Option, + /// When this [`BoxFragment`] is for content that has a baseline, this tracks + /// the first and last baselines of that content. This is used to propagate baselines + /// to things such as tables and inline formatting contexts. + pub baselines: Baselines, pub block_margins_collapsed_with_children: CollapsedBlockMargins, @@ -73,7 +74,6 @@ impl BoxFragment { border: LogicalSides, margin: LogicalSides, clearance: Option, - last_inflow_baseline_offset: Option, block_margins_collapsed_with_children: CollapsedBlockMargins, ) -> BoxFragment { let position = style.get_box().position; @@ -94,7 +94,6 @@ impl BoxFragment { border, margin, clearance, - last_inflow_baseline_offset, block_margins_collapsed_with_children, PhysicalSize::new(width_overconstrained, height_overconstrained), ) @@ -109,7 +108,6 @@ impl BoxFragment { border: LogicalSides, margin: LogicalSides, clearance: Option, - mut last_inflow_baseline_offset: Option, block_margins_collapsed_with_children: CollapsedBlockMargins, overconstrained: PhysicalSize, ) -> BoxFragment { @@ -126,10 +124,15 @@ impl BoxFragment { // > value) a block-level or inline-level block container that is a scroll container // > always has a last baseline set, whose baselines all correspond to its block-end // > margin edge. + // + // This applies even if there is no baseline set, so we unconditionally set the value here + // and ignore anything that is set via [`Self::with_baselines`]. + let mut baselines = Baselines::default(); if style.establishes_scroll_container() { - last_inflow_baseline_offset = Some( - content_rect.size.block + padding.block_end + border.block_end + margin.block_end, - ); + baselines.last = Some( + (content_rect.size.block + padding.block_end + border.block_end + margin.block_end) + .into(), + ) } BoxFragment { @@ -141,7 +144,7 @@ impl BoxFragment { border, margin, clearance, - last_baseline_offset: last_inflow_baseline_offset, + baselines, block_margins_collapsed_with_children, scrollable_overflow_from_children, overconstrained, @@ -149,6 +152,19 @@ impl BoxFragment { } } + pub fn with_baselines(mut self, baselines: Baselines) -> Self { + // From the https://drafts.csswg.org/css-align-3/#baseline-export section on "block containers": + // > However, for legacy reasons if its baseline-source is auto (the initial + // > value) a block-level or inline-level block container that is a scroll container + // > always has a last baseline set, whose baselines all correspond to its block-end + // > margin edge. + if !self.style.establishes_scroll_container() { + self.baselines.last = baselines.last; + } + self.baselines.first = baselines.first; + self + } + pub fn scrollable_overflow( &self, containing_block: &PhysicalRect, diff --git a/components/layout_2020/positioned.rs b/components/layout_2020/positioned.rs index 86a54a1c261..98bf5333a7c 100644 --- a/components/layout_2020/positioned.rs +++ b/components/layout_2020/positioned.rs @@ -706,7 +706,6 @@ impl HoistedAbsolutelyPositionedBox { None, /* clearance */ // We do not set the baseline offset, because absolutely positioned // elements are not inflow. - None, /* last_inflow_baseline_offset */ CollapsedBlockMargins::zero(), physical_overconstrained, ) diff --git a/components/layout_2020/table/layout.rs b/components/layout_2020/table/layout.rs index f33d85e3b38..d5fd4262c53 100644 --- a/components/layout_2020/table/layout.rs +++ b/components/layout_2020/table/layout.rs @@ -12,7 +12,7 @@ use style::values::generics::length::GenericLengthPercentageOrAuto::{Auto, Lengt use super::{Table, TableSlot, TableSlotCell}; use crate::context::LayoutContext; -use crate::formatting_contexts::IndependentLayout; +use crate::formatting_contexts::{Baselines, IndependentLayout}; use crate::fragment_tree::{BoxFragment, CollapsedBlockMargins, Fragment}; use crate::geom::{LogicalRect, LogicalSides, LogicalVec2}; use crate::positioned::{PositioningContext, PositioningContextLength}; @@ -1105,7 +1105,7 @@ impl Table { IndependentLayout { fragments, content_block_size, - last_inflow_baseline_offset: None, + baselines: Baselines::default(), } } } @@ -1167,11 +1167,8 @@ impl TableSlotCell { layout.border, LogicalSides::zero(), /* margin */ None, /* clearance */ - layout - .layout - .last_inflow_baseline_offset - .map(|baseline| baseline.into()), CollapsedBlockMargins::zero(), ) + .with_baselines(layout.layout.baselines) } } diff --git a/tests/wpt/meta/css/CSS2/positioning/position-relative-032.xht.ini b/tests/wpt/meta/css/CSS2/positioning/position-relative-032.xht.ini new file mode 100644 index 00000000000..fe049ea1775 --- /dev/null +++ b/tests/wpt/meta/css/CSS2/positioning/position-relative-032.xht.ini @@ -0,0 +1,2 @@ +[position-relative-032.xht] + expected: FAIL