Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224//! Serializes the dump format's style subset (`docs/dump-format.md`) from a node's//! computed style. This is the one place outside `style.rs` that still reads a Stylo//! type (`ComputedValues`); everything it returns is a plain string, matching how//! `getComputedStyle` serializes each property.//!//! Only the properties step 1 can check without layout are covered here (`handoff.md`)://! `width`, `height` and the `margin-*`/`padding-*` families are used values that need//! layout to exist, and are checked from step 3 on instead of here.//!//! [`is_display_none`] is the one accessor step 2 (box tree construction) needs: only//! whether `display` is `none`, not which of the many other keywords it is. A fuller//! spec-named `display` type belongs to box *construction* proper, once box kind (not//! just existence) matters.
use std::collections::BTreeMap;
use style::values::computed::{ BaselineShift as StyloBaselineShift, Clear as StyloClear, Float as StyloFloat, Inset as StyloInset, Length as StyloLength, LengthPercentage as StyloLengthPercentage, LineHeight as StyloLineHeight, Margin as StyloMargin, MaxSize as StyloMaxSize, PositionProperty as StyloPositionProperty, Size as StyloSize, TextAlign as StyloTextAlign, TransformOperation as StyloTransformOperation, ZIndex as StyloZIndex,};use style::values::generics::box_::BaselineShiftKeyword as StyloBaselineShiftKeyword;use style_traits::{CssWriter, ToCss};
use crate::node::NodeId;use crate::style::Document;
/// Whether the element's own computed `display` is `none`. Does not account for an/// ancestor's `display: none` -- callers that need "does this element ever get a box"/// (`roadmap.md` -> "Phase 1 -- Engine bootstrap" step 2) walk ancestors themselves.pub fn is_display_none(document: &Document, node: NodeId) -> bool { document.computed_style(node).is_some_and(|style| style.clone_display().is_none())}
/// The comparable style subset for one node.pub fn comparable_style(document: &Document, node: NodeId) -> BTreeMap<String, String> { let mut out = BTreeMap::new(); let Some(style) = document.computed_style(node) else { return out };
macro_rules! prop { ($key:literal, $value:expr) => { out.insert($key.to_string(), to_css_string(&$value)); }; } prop!("display", style.clone_display()); prop!("position", style.clone_position()); prop!("box-sizing", style.clone_box_sizing()); prop!("direction", style.clone_direction()); prop!("writing-mode", style.clone_writing_mode()); prop!("float", style.clone_float()); prop!("clear", style.clone_clear()); // CSSOM's resolved-value algorithm special-cases exactly these two properties: // "If the property is min-width or min-height, and its computed value is auto, // the resolved value is 0px" -- unlike max-width/max-height, where a computed // `none` resolves as `none`, unchanged. out.insert("min-width".to_string(), resolve_auto_to_zero(to_css_string(&style.clone_min_width()))); out.insert("min-height".to_string(), resolve_auto_to_zero(to_css_string(&style.clone_min_height()))); prop!("max-width", style.clone_max_width()); prop!("max-height", style.clone_max_height()); // CSSOM's resolved-value algorithm also special-cases the border-*-width // longhands: when the corresponding border-*-style computed value is `none` or // `hidden`, the resolved value is `0px` regardless of the declared width, since no // border edge paints (`style::values::computed::border`'s own `ToResolvedValue for // BorderSideWidth` applies the same rule). Every corpus recipe that sets a // border-width also sets `border-style: solid`, so this has not been exercised // through the oracle; a unit test covers it instead (this module's tests, and // `crates/layout/src/fragment.rs`'s geometry sibling). for (key, style_none_or_hidden, width) in [ ("border-top-width", style.clone_border_top_style().none_or_hidden(), style.clone_border_top_width()), ("border-right-width", style.clone_border_right_style().none_or_hidden(), style.clone_border_right_width()), ("border-bottom-width", style.clone_border_bottom_style().none_or_hidden(), style.clone_border_bottom_width()), ("border-left-width", style.clone_border_left_style().none_or_hidden(), style.clone_border_left_width()), ] { let value = if style_none_or_hidden { "0px".to_string() } else { to_css_string(&width) }; out.insert(key.to_string(), value); } prop!("overflow-x", style.clone_overflow_x()); prop!("overflow-y", style.clone_overflow_y()); out}
// -- Used-value accessors ------------------------------------------------------------//// `width`, `height`, `margin-*` and `padding-*` are checked from engine bootstrap step 3// on, against geometry rather than `getComputedStyle` (`handoff.md` -> "Step 1" and// "Step 3"). These return the computed value narrowed to what step 3's trivial layout// resolves -- a plain length in CSS pixels, or a marker for everything else -- rather// than a Stylo type, keeping the one-crate Stylo boundary (`roadmap.md` -> "DOM layer,// shaped by Stylo").
/// A computed `<length-percentage>`: `padding-*`. Step 3's trivial layout resolves only/// the plain-length case (`handoff.md` -> "Step 3: fragment tree, trivial layout"); a/// percentage lands in [`Percentage`](LengthPercentage::Percentage), which the/// percentage-resolution seam (`roadmap.md` -> "Phase 1 -- Engine bootstrap" step 4)/// resolves against a containing-block basis. `calc()` mixing a length and a percentage/// still lands in [`Unresolved`](LengthPercentage::Unresolved): Stylo's own/// `LengthPercentage` has no accessor that hands the two parts back separately, only/// `resolve(basis)`, so a `calc()` value cannot be carried as a plain number without a/// basis in hand already.#[derive(Debug, Clone, Copy, PartialEq)]pub enum LengthPercentage { Length(f64), /// A percentage, as the fraction Stylo itself already carries it at (`50%` is /// `0.5`, matching `style::values::computed::Percentage`'s own scale). Percentage(f64), /// `calc()` mixing a length and a percentage. Not produced by the current corpus. Unresolved,}
impl LengthPercentage { fn from_stylo(lp: &StyloLengthPercentage) -> Self { if let Some(l) = lp.to_length() { LengthPercentage::Length(f64::from(l.px())) } else if let Some(p) = lp.to_percentage() { LengthPercentage::Percentage(f64::from(p.0)) } else { LengthPercentage::Unresolved } }}
/// A computed `<length-percentage> | auto`: `width`, `height`, `margin-*`. Same/// resolution boundary as [`LengthPercentage`], plus the `auto` case those properties/// alone carry.#[derive(Debug, Clone, Copy, PartialEq)]pub enum LengthPercentageOrAuto { Auto, Length(f64), /// Same fraction scale as [`LengthPercentage::Percentage`]. Percentage(f64), /// `calc()` mixing a length and a percentage, an intrinsic-sizing keyword /// (`min-content` and similar, out of Phase 0's scope), or an anchor-positioning /// function (never generated by the current corpus). Unresolved,}
impl From<LengthPercentage> for LengthPercentageOrAuto { fn from(lp: LengthPercentage) -> Self { match lp { LengthPercentage::Length(px) => LengthPercentageOrAuto::Length(px), LengthPercentage::Percentage(p) => LengthPercentageOrAuto::Percentage(p), LengthPercentage::Unresolved => LengthPercentageOrAuto::Unresolved, } }}
fn size_to_length(size: &StyloSize) -> LengthPercentageOrAuto { match size { StyloSize::Auto => LengthPercentageOrAuto::Auto, StyloSize::LengthPercentage(lp) => LengthPercentage::from_stylo(&lp.0).into(), // Intrinsic-sizing keywords and anchor-positioning functions: intrinsic sizing is // out of Phase 0's scope (`roadmap.md` -> "Phase 0 -- Feasibility probe"), and // anchor positioning is never generated by the current corpus. _ => LengthPercentageOrAuto::Unresolved, }}
/// CSS 2.1 §9.3.1's positioning scheme, narrowed to the four values the absolute- and/// containing-block seams' corpora generate (`crates/harness/src/absolute_cases.rs`,/// `crates/harness/src/containing_block_cases.rs`). `Sticky` still maps to `Static`, the/// same way [`size_to_length`] maps an intrinsic-sizing keyword to `Unresolved` --/// collapsing a value this accessor's callers do not need to distinguish rather than/// growing a variant nothing reads; it is not a CSS 2.1 value at all.#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum Position { Static, Relative, Absolute, /// CSS 2.1 §9.3.1's `fixed`: out of flow like `Absolute`, but its containing block is /// the viewport rather than an ancestor box (§10.1 rule 3), which is what /// `layout::containing_block` resolves. Fixed,}
impl Position { /// Whether this scheme takes the box out of normal flow (CSS 2.1 §9.3.1): `absolute` /// and `fixed` both do, `static` and `relative` do not. The one test every "is this /// box laid out in flow" call site in `layout` makes, so adding `Fixed` did not mean /// spelling `== Position::Absolute || == Position::Fixed` at each of them. pub fn is_out_of_flow(self) -> bool { matches!(self, Position::Absolute | Position::Fixed) }}
pub fn position(document: &Document, node: NodeId) -> Position { document.computed_style(node).map_or(Position::Static, |style| match style.clone_position() { StyloPositionProperty::Static => Position::Static, StyloPositionProperty::Relative => Position::Relative, StyloPositionProperty::Absolute => Position::Absolute, StyloPositionProperty::Fixed => Position::Fixed, StyloPositionProperty::Sticky => Position::Static, })}
/// CSS 2.1 §9.5.1's `float`, for the float seam's corpus (`crates/harness/src/float_cases.rs`)./// The corpus is left-to-right only, so the logical `inline-start`/`inline-end` values/// map to `Left`/`Right` the way they resolve in `ltr` -- the same narrowing/// [`Position`] applies to `fixed`/`sticky`. CSS 2.1 §9.7's rule that `position:/// absolute` forces `float` to `none` is not applied here; `crates/layout/src/boxtree.rs`/// applies it where the box's kind is decided.#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum Float { None, Left, Right,}
pub fn float(document: &Document, node: NodeId) -> Float { document.computed_style(node).map_or(Float::None, |style| match style.clone_float() { StyloFloat::None => Float::None, StyloFloat::Left | StyloFloat::InlineStart => Float::Left, StyloFloat::Right | StyloFloat::InlineEnd => Float::Right, })}
/// CSS 2.1 §9.5.2's `clear`, narrowed the same way [`Float`] is: `inline-start`//// `inline-end` read as `Left`/`Right` for the left-to-right corpus.#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum Clear { None, Left, Right, Both,}
pub fn clear(document: &Document, node: NodeId) -> Clear { document.computed_style(node).map_or(Clear::None, |style| match style.clone_clear() { StyloClear::None => Clear::None, StyloClear::Left | StyloClear::InlineStart => Clear::Left, StyloClear::Right | StyloClear::InlineEnd => Clear::Right, StyloClear::Both => Clear::Both, })}
/// Whether the element's computed `display` is `flow-root` -- the one `display` value/// that establishes a new block formatting context on its own (CSS Display 3 §2.1),/// which the float seam's corpus uses beside `overflow: hidden` for the sibling that is/// placed next to a float rather than under it. Checked by serialization, the way/// [`is_border_box`] checks `box-sizing`, since `dom` deliberately has no full `display`/// type yet (this module's own doc comment).pub fn is_display_flow_root(document: &Document, node: NodeId) -> bool { document.computed_style(node).is_some_and(|style| to_css_string(&style.clone_display()) == "flow-root")}
/// The computed `display`, as far as box construction currently distinguishes it/// (`crates/layout/src/boxtree.rs`). Not a full CSS Display 3 type: the inside/outside/// pair is collapsed to the keywords box construction branches on, and everything/// else lands in [`Other`](Display::Other), the way [`Position`] collapses `sticky` into/// `Static` rather than growing a variant nothing reads. `display-contents` extends this/// enum when its own keyword starts to matter.////// Read by serialization, the same way [`is_display_none`] and [`is_display_flow_root`]/// already read the property; both keep working unchanged beside this, since neither is/// reimplemented in terms of it.#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum Display { Block, /// The one `display` value that establishes a block formatting context on its own /// (CSS Display 3 §2.1) -- [`is_display_flow_root`]'s answer, as a variant. FlowRoot, /// An atomic inline-level box that lays its own contents out as a block container /// (CSS Display 3 §2.1): the one inline-level box /// `crates/layout/src/inline_atomic.rs` places on a line. InlineBlock, /// A block-level table wrapper and its table box in one (CSS 2.1 §17.2): the box /// `crates/layout/src/table.rs` lays columns and rows out inside. `inline-table` is /// not this variant -- it lands in [`Other`](Display::Other), the way it did before /// this variant existed, since an inline-level table needs a shrink-to-fit width /// that no generated function computes yet (`specs/tables-fixed-layout.md`). Table, /// A row of cells inside a table (CSS 2.1 §17.2). Row groups /// (`table-row-group` and its header/footer siblings) are not this variant: they are /// out of scope and stay in [`Other`](Display::Other). TableRow, /// One cell (CSS 2.1 §17.2). Establishes a block formatting context for its own /// contents, the way an `inline-block` does (§9.4.1). TableCell, /// Generates no box at all, and neither does anything in its subtree (CSS 2.1 /// §9.2.5) -- [`is_display_none`]'s answer, as a variant. None, /// Every other keyword: `inline`, `inline-table`, `table-row-group`, /// `table-column`, `table-caption`, `flex`, `grid`, `contents`, `list-item` and the /// rest. Each generates a box, and box construction treats it as block-level, which /// is what the engine did for all of them before this enum existed. Other,}
impl Display { /// Whether a box with this `display` is inline-level -- participates in an inline /// formatting context rather than stacking in normal flow (CSS 2.1 §9.2.2). Only /// [`InlineBlock`](Display::InlineBlock) is, in this build: `inline` itself is out of /// scope until there is text to put in it (`specs/inline-atomic.md`), and /// `inline-table` is out of scope too (this enum's own [`Table`](Display::Table) /// doc). pub fn is_inline_level(self) -> bool { matches!(self, Display::InlineBlock) }}
pub fn display(document: &Document, node: NodeId) -> Display { document.computed_style(node).map_or(Display::Block, |style| { match to_css_string(&style.clone_display()).as_str() { "none" => Display::None, "block" => Display::Block, "flow-root" => Display::FlowRoot, "inline-block" => Display::InlineBlock, "table" => Display::Table, "table-row" => Display::TableRow, "table-cell" => Display::TableCell, _ => Display::Other, } })}
/// CSS 2.1 §17.5.2's two table layout algorithms. `Fixed` is the one/// `crates/layout/src/table.rs` implements; `Auto` reads every cell's content and is out/// of scope, so a table whose used algorithm is `Auto` keeps stacking its contents the/// way the engine did before that module existed.#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum TableLayout { Auto, Fixed,}
/// The computed `table-layout`. Read by serialization, matching [`is_border_box`]'s/// pattern for a plain two-keyword property. `auto` is the initial value, so an element/// with no computed style at all reads as `Auto`.pub fn table_layout(document: &Document, node: NodeId) -> TableLayout { document.computed_style(node).map_or(TableLayout::Auto, |style| { match to_css_string(&style.clone_table_layout()).as_str() { "fixed" => TableLayout::Fixed, _ => TableLayout::Auto, } })}
/// The computed `border-spacing` as `(horizontal, vertical)` in CSS pixels (CSS 2.1/// §17.6.1): the space between adjacent cell borders, and between the table's own/// content edge and the cells along it, in the separated-borders model. The property is/// inherited and its grammar allows no percentage, so both components are already/// absolute lengths by the time the value computes; the one-value form computes to the/// same length on both axes, which is why this returns a pair rather than an enum./// Zero for an element with no computed style, which is the initial value.pub fn border_spacing(document: &Document, node: NodeId) -> (f64, f64) { document.computed_style(node).map_or((0.0, 0.0), |style| { let spacing = style.clone_border_spacing(); (f64::from(spacing.0.width.0.px()), f64::from(spacing.0.height.0.px())) })}
/// CSS 2.1 §9.9.1's `z-index`: `auto` or an integer. Only meaningful on a positioned box/// (an unpositioned box's `z-index` is ignored by CSS 2.1 and by every engine), but the/// accessor reports the computed value regardless and leaves that rule to the/// stacking-order seam (`crates/layout/src/stacking.rs`), whose candidates must apply it.#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum ZIndex { Auto, Integer(i32),}
pub fn z_index(document: &Document, node: NodeId) -> ZIndex { document.computed_style(node).map_or(ZIndex::Auto, |style| match style.clone_z_index() { StyloZIndex::Auto => ZIndex::Auto, StyloZIndex::Integer(i) => ZIndex::Integer(i), })}
/// The computed `opacity`, clamped to `[0, 1]` the way the computed value is defined./// `1.0` for a node without computed style. Read by the stacking-order seam only for the/// one rule it has in scope: any value below `1` establishes a stacking context.pub fn opacity(document: &Document, node: NodeId) -> f64 { document .computed_style(node) .map_or(1.0, |style| f64::from(style.clone_opacity()).clamp(0.0, 1.0))}
/// A computed `<length-percentage> | auto` for `top`/`right`/`bottom`/`left` (CSS 2.1/// §9.3.2). Stylo's own computed type for these four properties, `Inset`, is a distinct/// type from `Margin` (an extra `AnchorFunction` variant `Margin` does not have) but/// resolves to [`LengthPercentageOrAuto`] the same way: `Auto` passes through, a/// `LengthPercentage` narrows through [`LengthPercentage::from_stylo`], and every/// anchor-positioning function -- never produced by the current corpus -- lands in/// `Unresolved`, the same catch-all [`margin_to_length`]'s own anchor-function arm uses.fn inset_to_length(inset: &StyloInset) -> LengthPercentageOrAuto { match inset { StyloInset::Auto => LengthPercentageOrAuto::Auto, StyloInset::LengthPercentage(lp) => LengthPercentage::from_stylo(lp).into(), StyloInset::AnchorFunction(_) | StyloInset::AnchorSizeFunction(_) | StyloInset::AnchorContainingCalcFunction(_) => LengthPercentageOrAuto::Unresolved, }}
pub fn top(document: &Document, node: NodeId) -> LengthPercentageOrAuto { document.computed_style(node).map_or(LengthPercentageOrAuto::Unresolved, |style| { inset_to_length(&style.clone_top()) })}
pub fn right(document: &Document, node: NodeId) -> LengthPercentageOrAuto { document.computed_style(node).map_or(LengthPercentageOrAuto::Unresolved, |style| { inset_to_length(&style.clone_right()) })}
pub fn bottom(document: &Document, node: NodeId) -> LengthPercentageOrAuto { document.computed_style(node).map_or(LengthPercentageOrAuto::Unresolved, |style| { inset_to_length(&style.clone_bottom()) })}
pub fn left(document: &Document, node: NodeId) -> LengthPercentageOrAuto { document.computed_style(node).map_or(LengthPercentageOrAuto::Unresolved, |style| { inset_to_length(&style.clone_left()) })}
fn margin_to_length(margin: &StyloMargin) -> LengthPercentageOrAuto { match margin { StyloMargin::Auto => LengthPercentageOrAuto::Auto, StyloMargin::LengthPercentage(lp) => LengthPercentage::from_stylo(lp).into(), // Anchor-positioning functions: not produced by the current corpus. StyloMargin::AnchorSizeFunction(_) | StyloMargin::AnchorContainingCalcFunction(_) => { LengthPercentageOrAuto::Unresolved } }}
pub fn width(document: &Document, node: NodeId) -> LengthPercentageOrAuto { document.computed_style(node).map_or(LengthPercentageOrAuto::Unresolved, |style| { size_to_length(&style.clone_width()) })}
pub fn height(document: &Document, node: NodeId) -> LengthPercentageOrAuto { document.computed_style(node).map_or(LengthPercentageOrAuto::Unresolved, |style| { size_to_length(&style.clone_height()) })}
pub fn margin_top(document: &Document, node: NodeId) -> LengthPercentageOrAuto { document.computed_style(node).map_or(LengthPercentageOrAuto::Unresolved, |style| { margin_to_length(&style.clone_margin_top()) })}
pub fn margin_right(document: &Document, node: NodeId) -> LengthPercentageOrAuto { document.computed_style(node).map_or(LengthPercentageOrAuto::Unresolved, |style| { margin_to_length(&style.clone_margin_right()) })}
pub fn margin_bottom(document: &Document, node: NodeId) -> LengthPercentageOrAuto { document.computed_style(node).map_or(LengthPercentageOrAuto::Unresolved, |style| { margin_to_length(&style.clone_margin_bottom()) })}
pub fn margin_left(document: &Document, node: NodeId) -> LengthPercentageOrAuto { document.computed_style(node).map_or(LengthPercentageOrAuto::Unresolved, |style| { margin_to_length(&style.clone_margin_left()) })}
pub fn padding_top(document: &Document, node: NodeId) -> LengthPercentage { document.computed_style(node).map_or(LengthPercentage::Unresolved, |style| { LengthPercentage::from_stylo(&style.clone_padding_top().0) })}
pub fn padding_right(document: &Document, node: NodeId) -> LengthPercentage { document.computed_style(node).map_or(LengthPercentage::Unresolved, |style| { LengthPercentage::from_stylo(&style.clone_padding_right().0) })}
pub fn padding_bottom(document: &Document, node: NodeId) -> LengthPercentage { document.computed_style(node).map_or(LengthPercentage::Unresolved, |style| { LengthPercentage::from_stylo(&style.clone_padding_bottom().0) })}
pub fn padding_left(document: &Document, node: NodeId) -> LengthPercentage { document.computed_style(node).map_or(LengthPercentage::Unresolved, |style| { LengthPercentage::from_stylo(&style.clone_padding_left().0) })}
/// `border-*-width` is already a resolved absolute length by the time it reaches/// `ComputedValues` -- CSS's grammar allows no percentage and no `auto` on this property/// -- so this returns a definite pixel value directly rather than through/// [`LengthPercentage`], except that a `none`/`hidden` border-style zeroes it: no border/// edge paints in that case, so the used value layout must consume is `0px` regardless/// of the declared width (`comparable_style`'s border-width entries apply the identical/// rule, with the CSSOM citation).fn border_width_px(border_side_width: style::values::computed::BorderSideWidth, style_none_or_hidden: bool) -> f64 { if style_none_or_hidden { 0.0 } else { f64::from(StyloLength::from(border_side_width.0).px()) }}
pub fn border_top_width(document: &Document, node: NodeId) -> f64 { document.computed_style(node).map_or(0.0, |style| { border_width_px(style.clone_border_top_width(), style.clone_border_top_style().none_or_hidden()) })}
pub fn border_right_width(document: &Document, node: NodeId) -> f64 { document.computed_style(node).map_or(0.0, |style| { border_width_px(style.clone_border_right_width(), style.clone_border_right_style().none_or_hidden()) })}
pub fn border_bottom_width(document: &Document, node: NodeId) -> f64 { document.computed_style(node).map_or(0.0, |style| { border_width_px(style.clone_border_bottom_width(), style.clone_border_bottom_style().none_or_hidden()) })}
pub fn border_left_width(document: &Document, node: NodeId) -> f64 { document.computed_style(node).map_or(0.0, |style| { border_width_px(style.clone_border_left_width(), style.clone_border_left_style().none_or_hidden()) })}
/// A paint color resolved to sRGB, as `[red, green, blue, alpha]` with each channel in/// `0..=255`. Every color property in CSS computes to a value that may still be/// `currentcolor` or a `color-mix()`, and may sit in a color space other than sRGB; this/// is the form a rasterizer can write into a pixel buffer without knowing any of that.pub type Rgba = [u8; 4];
/// Fully transparent, the CSS initial value of `background-color` and the value this/// module reports for a border side that paints no edge. Zero alpha is the signal a/// caller checks: it means "leave whatever is already there alone", not "paint black".pub const TRANSPARENT: Rgba = [0, 0, 0, 0];
/// Resolves a computed `<color>` to [`Rgba`]. Two steps the computed value still owes:/// `currentcolor`, `color-mix()` and `contrast-color()` resolve against the element's own/// `color` (Stylo's `resolve_to_absolute`), and the resulting absolute color is converted/// into sRGB, since a color authored in any other space computes in that space. The/// 0.0-1.0 channels then scale to bytes exactly as Stylo's own `to_nscolor` does. A/// missing component (carried as NaN, the `none` keyword's representation) reads as zero,/// which is what `none` means once a color is actually painted.fn to_rgba(color: style::color::AbsoluteColor) -> Rgba { let srgb = color.to_color_space(style::color::ColorSpace::Srgb); let channel = |value: f32| { if value.is_nan() { 0 } else { (value * 255.0).round().clamp(0.0, 255.0) as u8 } }; [ channel(srgb.components.0), channel(srgb.components.1), channel(srgb.components.2), channel(srgb.alpha), ]}
pub fn background_color(document: &Document, node: NodeId) -> Rgba { document.computed_style(node).map_or(TRANSPARENT, |style| { to_rgba(style.clone_background_color().resolve_to_absolute(&style.clone_color())) })}
/// A `none`/`hidden` border-style paints no edge at all, so its color is reported fully/// transparent rather than as the declared value -- the same rule/// [`border_width_px`] applies to the width, and the one `comparable_style`'s/// border-width entries already apply with the CSSOM citation. Without it a caller would/// have to re-read `border-*-style` itself to know whether the color meant anything.pub fn border_top_color(document: &Document, node: NodeId) -> Rgba { document.computed_style(node).map_or(TRANSPARENT, |style| { if style.clone_border_top_style().none_or_hidden() { TRANSPARENT } else { to_rgba(style.clone_border_top_color().resolve_to_absolute(&style.clone_color())) } })}
pub fn border_right_color(document: &Document, node: NodeId) -> Rgba { document.computed_style(node).map_or(TRANSPARENT, |style| { if style.clone_border_right_style().none_or_hidden() { TRANSPARENT } else { to_rgba(style.clone_border_right_color().resolve_to_absolute(&style.clone_color())) } })}
pub fn border_bottom_color(document: &Document, node: NodeId) -> Rgba { document.computed_style(node).map_or(TRANSPARENT, |style| { if style.clone_border_bottom_style().none_or_hidden() { TRANSPARENT } else { to_rgba(style.clone_border_bottom_color().resolve_to_absolute(&style.clone_color())) } })}
pub fn border_left_color(document: &Document, node: NodeId) -> Rgba { document.computed_style(node).map_or(TRANSPARENT, |style| { if style.clone_border_left_style().none_or_hidden() { TRANSPARENT } else { to_rgba(style.clone_border_left_color().resolve_to_absolute(&style.clone_color())) } })}
/// Whether `box-sizing` is `border-box` rather than `content-box`. Compared by its CSS/// serialization, matching how the rest of this module reads keyword properties, since/// `box-sizing`'s computed type is a plain two-value keyword enum with no numeric part.pub fn is_border_box(document: &Document, node: NodeId) -> bool { document.computed_style(node).is_some_and(|style| to_css_string(&style.clone_box_sizing()) == "border-box")}
/// Whether the element's computed `direction` is `rtl` (`ltr` is the initial value, so/// anything else -- including no computed style at all -- reads as not-rtl). Compared by/// CSS serialization, matching [`is_border_box`]'s pattern for a plain keyword property./// Added for `crates/layout/src/width.rs`'s block-width resolver/// (`roadmap.md` -> "Phase 1 — Engine bootstrap" step 4), CSS 2.1 10.3.3's/// over-constraint rule being the one place block width resolution reads `direction`.pub fn is_rtl(document: &Document, node: NodeId) -> bool { document.computed_style(node).is_some_and(|style| to_css_string(&style.clone_direction()) == "rtl")}
/// `min-width` as a definite pixel length, or `None` for `auto`, an intrinsic-sizing/// keyword, or a percentage -- everything CSS 2.1 10.4 clamping resolves that this/// accessor's first caller (`crates/layout/src/width.rs`) does not yet clamp against./// `min-width` and `width` share the same computed type (`style::values::computed::Size`),/// so this reuses [`size_to_length`] rather than a `min`-specific conversion.pub fn min_width(document: &Document, node: NodeId) -> Option<f64> { document.computed_style(node).and_then(|style| match size_to_length(&style.clone_min_width()) { LengthPercentageOrAuto::Length(px) => Some(px), LengthPercentageOrAuto::Auto | LengthPercentageOrAuto::Percentage(_) | LengthPercentageOrAuto::Unresolved => { None } })}
/// `max-width` as a definite pixel length, or `None` for `none`, an intrinsic-sizing/// keyword, or a percentage. `max-width`'s computed type (`MaxSize`) has a `None` variant/// in place of `Size`'s `Auto`, so it gets its own conversion rather than reusing/// [`size_to_length`].pub fn max_width(document: &Document, node: NodeId) -> Option<f64> { document.computed_style(node).and_then(|style| match style.clone_max_width() { StyloMaxSize::LengthPercentage(lp) => match LengthPercentage::from_stylo(&lp.0) { LengthPercentage::Length(px) => Some(px), LengthPercentage::Percentage(_) | LengthPercentage::Unresolved => None, }, _ => None, })}
/// The computed `overflow-x`/`overflow-y` keyword, serialized the same way/// `comparable_style` already does inline for the dump format. Added for/// `crates/logic` (`docs/margin-collapse-experiment.md`): CSS 2.1 9.4.1 makes any value/// other than `visible` on either axis establish a new block formatting context, which/// blocks margin collapsing between a box and its children (8.3.1). Not part of the/// dump-format style subset's own accessor set before this, since no consumer needed the/// two properties individually until now.pub fn overflow_x(document: &Document, node: NodeId) -> String { document .computed_style(node) .map_or_else(|| "visible".to_string(), |style| to_css_string(&style.clone_overflow_x()))}
pub fn overflow_y(document: &Document, node: NodeId) -> String { document .computed_style(node) .map_or_else(|| "visible".to_string(), |style| to_css_string(&style.clone_overflow_y()))}
/// One 2D function from a computed `transform` list, as plain data: lengths in CSS/// pixels, percentages as fractions of the reference box (`50%` is `0.5`, the same scale/// [`LengthPercentage::Percentage`] uses), angles in radians. CSS Transforms 1 §3 defines/// the list; `layout::transform` turns it into a matrix.////// Only the 2D functions are carried. A list holding a 3D function or `perspective`/// lands in [`Unsupported`](TransformFunction::Unsupported) at that position, so a/// consumer can tell "a function this build does not model" from "no transform at all"/// rather than silently dropping it (`specs/transforms2d.md`'s own "Out" list).#[derive(Debug, Clone, Copy, PartialEq)]pub enum TransformFunction { /// `translate(x, y)`, `translateX(x)` and `translateY(y)`, the missing axis zero. /// Percentages are of the reference box's own width (x) and height (y). Translate { x: LengthPercentage, y: LengthPercentage }, /// `scale(sx, sy)`, `scaleX(sx)` and `scaleY(sy)`, the missing axis `1.0`. Scale { x: f64, y: f64 }, /// `rotate(angle)`, in radians, positive clockwise in CSS's y-down coordinates. Rotate { radians: f64 }, /// `skew(ax, ay)`, `skewX(ax)` and `skewY(ay)`, in radians, the missing axis zero. Skew { x_radians: f64, y_radians: f64 }, /// `matrix(a, b, c, d, e, f)`, already the affine matrix CSS Transforms 1 §11 names. Matrix { a: f64, b: f64, c: f64, d: f64, e: f64, f: f64 }, /// A 3D function or `perspective()`: out of scope, and never generated by the /// transform corpus (`crates/harness/src/transform_cases.rs`). Unsupported,}
fn transform_function_from_stylo(op: &StyloTransformOperation) -> TransformFunction { let zero = LengthPercentage::Length(0.0); match op { StyloTransformOperation::Translate(x, y) => TransformFunction::Translate { x: LengthPercentage::from_stylo(x), y: LengthPercentage::from_stylo(y), }, StyloTransformOperation::TranslateX(x) => { TransformFunction::Translate { x: LengthPercentage::from_stylo(x), y: zero } } StyloTransformOperation::TranslateY(y) => { TransformFunction::Translate { x: zero, y: LengthPercentage::from_stylo(y) } } StyloTransformOperation::Scale(x, y) => { TransformFunction::Scale { x: f64::from(*x), y: f64::from(*y) } } StyloTransformOperation::ScaleX(x) => TransformFunction::Scale { x: f64::from(*x), y: 1.0 }, StyloTransformOperation::ScaleY(y) => TransformFunction::Scale { x: 1.0, y: f64::from(*y) }, StyloTransformOperation::Rotate(angle) => TransformFunction::Rotate { radians: angle.radians64() }, StyloTransformOperation::Skew(ax, ay) => { TransformFunction::Skew { x_radians: ax.radians64(), y_radians: ay.radians64() } } StyloTransformOperation::SkewX(ax) => { TransformFunction::Skew { x_radians: ax.radians64(), y_radians: 0.0 } } StyloTransformOperation::SkewY(ay) => { TransformFunction::Skew { x_radians: 0.0, y_radians: ay.radians64() } } StyloTransformOperation::Matrix(m) => TransformFunction::Matrix { a: f64::from(m.a), b: f64::from(m.b), c: f64::from(m.c), d: f64::from(m.d), e: f64::from(m.e), f: f64::from(m.f), }, _ => TransformFunction::Unsupported, }}
/// The computed `transform` as a list of plain-data functions, in the order they were/// written -- CSS Transforms 1 §11 composes them left to right. An empty list is/// `transform: none`, which is the initial value and what every recipe in/// `crates/harness/src/cases.rs` leaves it at.pub fn transform(document: &Document, node: NodeId) -> Vec<TransformFunction> { document.computed_style(node).map_or_else(Vec::new, |style| { style.clone_transform().0.iter().map(transform_function_from_stylo).collect() })}
/// The computed `transform-origin`'s horizontal and vertical components, each a length/// in CSS pixels or a percentage of the reference box on that axis. The initial value is/// `50% 50%`, which Stylo already resolves keywords (`left`, `center`, `top`, ...) and/// one-value forms down to, so this accessor never sees a keyword. The third component,/// `z`, stays 0 and is not carried: 3D transforms are out of scope/// (`specs/transforms2d.md`).pub fn transform_origin(document: &Document, node: NodeId) -> (LengthPercentage, LengthPercentage) { document.computed_style(node).map_or( (LengthPercentage::Percentage(0.5), LengthPercentage::Percentage(0.5)), |style| { let origin = style.clone_transform_origin(); (LengthPercentage::from_stylo(&origin.horizontal), LengthPercentage::from_stylo(&origin.vertical)) }, )}
/// CSS 2.1 §10.8.1's `vertical-align`, narrowed to the values/// `crates/layout/src/inline_atomic.rs` places on a line. The font-dependent keywords/// (`sub`, `super`, `text-top`, `text-bottom`) land in [`Other`](VerticalAlign::Other),/// the same narrowing [`Position`] applies to `sticky`: this build has no font metrics/// to resolve them against, and the inline-atomic corpus never generates one.////// [`Length`](VerticalAlign::Length) carries both the `<length>` and the `<percentage>`/// form, positive raising the box above the parent's baseline; a percentage is of the/// box's own `line-height`, which `crates/layout/src/fragment.rs` resolves before the/// value reaches a candidate.#[derive(Debug, Clone, Copy, PartialEq)]pub enum VerticalAlign { Baseline, Top, Bottom, Middle, Length(LengthPercentage), Other,}
/// The computed `vertical-align`. This build's Stylo models the property as CSS Inline/// 3's shorthand over `alignment-baseline`/`baseline-shift`/`baseline-source` rather than/// as one CSS 2.1 value, so the two longhands that carry the CSS 2.1 keywords are read/// back and recombined here: `middle` is an `alignment-baseline`, `top` and `bottom` are/// `baseline-shift` keywords, and a `<length>` or `<percentage>` is a `baseline-shift`/// length. `baseline-source` carries `first`/`last`, which CSS 2.1 has no counterpart for/// and this function does not read.////// A shift of exactly zero beside an `alignment-baseline` of `baseline` is the initial/// value, which is `vertical-align: baseline`.pub fn vertical_align(document: &Document, node: NodeId) -> VerticalAlign { document.computed_style(node).map_or(VerticalAlign::Baseline, |style| { match style.clone_baseline_shift() { StyloBaselineShift::Keyword(StyloBaselineShiftKeyword::Top) => return VerticalAlign::Top, StyloBaselineShift::Keyword(StyloBaselineShiftKeyword::Bottom) => return VerticalAlign::Bottom, StyloBaselineShift::Keyword(StyloBaselineShiftKeyword::Center) => return VerticalAlign::Middle, StyloBaselineShift::Keyword(_) => return VerticalAlign::Other, StyloBaselineShift::Length(lp) => match LengthPercentage::from_stylo(&lp) { LengthPercentage::Length(px) if px == 0.0 => {} resolved => return VerticalAlign::Length(resolved), }, } match to_css_string(&style.clone_alignment_baseline()).as_str() { "baseline" => VerticalAlign::Baseline, "middle" => VerticalAlign::Middle, _ => VerticalAlign::Other, } })}
/// The computed `line-height` in CSS pixels, or `None` for `normal` -- the one value/// whose used height comes from the font rather than from the cascade, and the one/// `crates/layout/src/inline_atomic.rs` puts out of scope. A `<number>` has already been/// multiplied by the element's own `font-size` by the time the value computes, and so has/// a `<percentage>`, so both arrive here as a plain length.pub fn line_height(document: &Document, node: NodeId) -> Option<f64> { document.computed_style(node).and_then(|style| match style.clone_line_height() { StyloLineHeight::Normal => None, StyloLineHeight::Length(px) => Some(f64::from(px.0.px())), StyloLineHeight::Number(n) => Some(f64::from(n.0) * f64::from(style.clone_font_size().used_size().px())), })}
/// CSS 2.1 §16.2's `text-align`, narrowed to the three values/// `crates/layout/src/inline_atomic.rs` distributes a line box's leftover space by./// `justify` and everything else land in [`Other`](TextAlign::Other). The initial/// computed value is `start`, which in the left-to-right corpus this build measures is/// [`Left`](TextAlign::Left) -- the same narrowing [`Float`] applies to `inline-start`.#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum TextAlign { Left, Right, Center, Other,}
pub fn text_align(document: &Document, node: NodeId) -> TextAlign { document.computed_style(node).map_or(TextAlign::Left, |style| match style.clone_text_align() { StyloTextAlign::Start | StyloTextAlign::Left | StyloTextAlign::MozLeft => TextAlign::Left, StyloTextAlign::End | StyloTextAlign::Right | StyloTextAlign::MozRight => TextAlign::Right, StyloTextAlign::Center | StyloTextAlign::MozCenter => TextAlign::Center, StyloTextAlign::Justify => TextAlign::Other, })}
fn to_css_string<T: ToCss>(value: &T) -> String { let mut s = String::new(); value.to_css(&mut CssWriter::new(&mut s)).expect("ToCss writing to a String never fails"); s}
fn resolve_auto_to_zero(computed: String) -> String { if computed == "auto" { "0px".to_string() } else { computed }}
#[cfg(test)]mod tests { use super::*;
/// The element carrying `id`, found the same way the check binaries correlate /// nodes: walking `elements_in_document_order` and reading the parsed attribute /// back off `Document::nodes()` (`crates/dom/src/node.rs` `ElementData::attr`). fn find(document: &Document, id: &str) -> NodeId { document .elements_in_document_order() .into_iter() .find(|&e| document.nodes()[e.index()].as_element().unwrap().attr("id") == Some(id)) .unwrap_or_else(|| panic!("no element with id={id:?}")) }
#[test] fn comparable_style_reports_declared_keyword_properties() { let html = "<!doctype html><style>#a{position:relative;float:left;clear:both;\ overflow-x:hidden;overflow-y:scroll;box-sizing:border-box}</style>\ <div id=a></div>"; let document = Document::parse(html); let style = comparable_style(&document, find(&document, "a")); assert_eq!(style.get("display").map(String::as_str), Some("block")); assert_eq!(style.get("position").map(String::as_str), Some("relative")); assert_eq!(style.get("float").map(String::as_str), Some("left")); assert_eq!(style.get("clear").map(String::as_str), Some("both")); assert_eq!(style.get("overflow-x").map(String::as_str), Some("hidden")); assert_eq!(style.get("overflow-y").map(String::as_str), Some("scroll")); assert_eq!(style.get("box-sizing").map(String::as_str), Some("border-box")); }
#[test] fn comparable_style_resolves_auto_min_size_to_zero_but_leaves_max_size_as_none() { let document = Document::parse("<!doctype html><div id=a></div>"); let style = comparable_style(&document, find(&document, "a")); assert_eq!(style.get("min-width").map(String::as_str), Some("0px")); assert_eq!(style.get("min-height").map(String::as_str), Some("0px")); assert_eq!(style.get("max-width").map(String::as_str), Some("none")); assert_eq!(style.get("max-height").map(String::as_str), Some("none")); }
#[test] fn is_display_none_reports_only_the_elements_own_computed_display() { let html = "<!doctype html><style>.hidden{display:none}</style>\ <div class=hidden><div id=child></div></div>"; let document = Document::parse(html); let hidden = document .elements_in_document_order() .into_iter() .find(|&e| document.nodes()[e.index()].as_element().unwrap().attr("class") == Some("hidden")) .unwrap(); let child = find(&document, "child"); assert!(is_display_none(&document, hidden)); assert!( !is_display_none(&document, child), "the child's own display is still block even though an ancestor is display:none" ); }
#[test] fn width_height_and_margin_accessors_resolve_plain_lengths_and_auto() { let html = "<!doctype html><style>#a{width:200px;height:auto;margin-left:auto;\ margin-right:10px;margin-top:0px;margin-bottom:-5px}</style><div id=a></div>"; let document = Document::parse(html); let a = find(&document, "a"); assert_eq!(width(&document, a), LengthPercentageOrAuto::Length(200.0)); assert_eq!(height(&document, a), LengthPercentageOrAuto::Auto); assert_eq!(margin_left(&document, a), LengthPercentageOrAuto::Auto); assert_eq!(margin_right(&document, a), LengthPercentageOrAuto::Length(10.0)); assert_eq!(margin_top(&document, a), LengthPercentageOrAuto::Length(0.0)); assert_eq!(margin_bottom(&document, a), LengthPercentageOrAuto::Length(-5.0), "negative margins are legal"); }
#[test] fn body_defaults_to_the_user_agent_stylesheets_eight_pixel_margin() { let document = Document::parse("<!doctype html><body></body>"); let body = document.elements_in_document_order().into_iter().find(|&e| e.tag() == "BODY").expect("body exists"); assert_eq!(margin_top(&document, body), LengthPercentageOrAuto::Length(8.0)); assert_eq!(margin_right(&document, body), LengthPercentageOrAuto::Length(8.0)); assert_eq!(margin_bottom(&document, body), LengthPercentageOrAuto::Length(8.0)); assert_eq!(margin_left(&document, body), LengthPercentageOrAuto::Length(8.0)); }
#[test] fn an_author_margin_on_body_overrides_the_user_agent_default() { let document = Document::parse("<!doctype html><style>body{margin:0}</style><body></body>"); let body = document.elements_in_document_order().into_iter().find(|&e| e.tag() == "BODY").expect("body exists"); assert_eq!(margin_top(&document, body), LengthPercentageOrAuto::Length(0.0)); assert_eq!(margin_right(&document, body), LengthPercentageOrAuto::Length(0.0)); assert_eq!(margin_bottom(&document, body), LengthPercentageOrAuto::Length(0.0)); assert_eq!(margin_left(&document, body), LengthPercentageOrAuto::Length(0.0)); }
#[test] fn a_style_attribute_with_no_matching_stylesheet_rule_applies_on_its_own() { let document = Document::parse("<!doctype html><div id=a style=\"margin-left:15px\"></div>"); let a = find(&document, "a"); assert_eq!(margin_left(&document, a), LengthPercentageOrAuto::Length(15.0)); }
#[test] fn an_inline_declaration_overrides_a_stylesheet_rule_but_not_a_stylesheet_important_one() { let html = "<!doctype html><style>#a{margin-left:5px}#b{margin-left:5px !important}</style>\ <div id=a style=\"margin-left:15px\"></div>\ <div id=b style=\"margin-left:15px\"></div>"; let document = Document::parse(html); assert_eq!( margin_left(&document, find(&document, "a")), LengthPercentageOrAuto::Length(15.0), "a plain inline declaration overrides a same-property stylesheet rule" ); assert_eq!( margin_left(&document, find(&document, "b")), LengthPercentageOrAuto::Length(5.0), "a stylesheet !important declaration still overrides a plain inline one" ); }
#[test] fn position_reports_the_computed_keyword_and_maps_sticky_to_static() { let html = "<!doctype html><style>#a{position:relative}#b{position:absolute}\ #c{position:fixed}#d{position:sticky}</style>\ <div id=a></div><div id=b></div><div id=c></div><div id=d></div><div id=e></div>"; let document = Document::parse(html); assert_eq!(position(&document, find(&document, "a")), Position::Relative); assert_eq!(position(&document, find(&document, "b")), Position::Absolute); assert_eq!(position(&document, find(&document, "c")), Position::Fixed); assert_eq!(position(&document, find(&document, "d")), Position::Static, "sticky maps to static"); assert_eq!(position(&document, find(&document, "e")), Position::Static, "static is the initial value"); }
#[test] fn absolute_and_fixed_are_the_out_of_flow_schemes() { assert!(Position::Absolute.is_out_of_flow()); assert!(Position::Fixed.is_out_of_flow()); assert!(!Position::Static.is_out_of_flow()); assert!(!Position::Relative.is_out_of_flow()); }
#[test] fn inset_accessors_resolve_lengths_percentages_and_auto() { let html = "<!doctype html><style>#a{top:12px;right:50%;bottom:auto;left:-4px}</style><div id=a></div>"; let document = Document::parse(html); let a = find(&document, "a"); assert_eq!(top(&document, a), LengthPercentageOrAuto::Length(12.0)); assert_eq!(right(&document, a), LengthPercentageOrAuto::Percentage(0.5)); assert_eq!(bottom(&document, a), LengthPercentageOrAuto::Auto); assert_eq!(left(&document, a), LengthPercentageOrAuto::Length(-4.0), "negative insets are legal"); }
#[test] fn percentage_width_is_reported_as_a_fraction() { let document = Document::parse("<!doctype html><style>#a{width:50%}</style><div id=a></div>"); assert_eq!(width(&document, find(&document, "a")), LengthPercentageOrAuto::Percentage(0.5)); }
#[test] fn calc_mixing_a_length_and_a_percentage_is_reported_as_unresolved() { let document = Document::parse("<!doctype html><style>#a{width:calc(50% + 10px)}</style><div id=a></div>"); assert_eq!(width(&document, find(&document, "a")), LengthPercentageOrAuto::Unresolved); }
#[test] fn padding_accessors_resolve_plain_lengths() { let html = "<!doctype html><style>#a{padding-top:5px;padding-right:2.5px;\ padding-bottom:0px;padding-left:12px}</style><div id=a></div>"; let document = Document::parse(html); let a = find(&document, "a"); assert_eq!(padding_top(&document, a), LengthPercentage::Length(5.0)); assert_eq!(padding_right(&document, a), LengthPercentage::Length(2.5)); assert_eq!(padding_bottom(&document, a), LengthPercentage::Length(0.0)); assert_eq!(padding_left(&document, a), LengthPercentage::Length(12.0)); }
#[test] fn border_width_is_zero_without_a_border_style_even_when_a_width_is_declared() { let document = Document::parse("<!doctype html><style>#a{border-top-width:5px}</style><div id=a></div>"); assert_eq!( border_top_width(&document, find(&document, "a")), 0.0, "border-style defaults to none, which computes border-width to 0 regardless of the declared width" ); }
#[test] fn border_width_reflects_the_declared_width_once_a_style_is_set() { let document = Document::parse("<!doctype html><style>#a{border-width:3px;border-style:solid}</style><div id=a></div>"); let a = find(&document, "a"); assert_eq!(border_top_width(&document, a), 3.0); assert_eq!(border_right_width(&document, a), 3.0); assert_eq!(border_bottom_width(&document, a), 3.0); assert_eq!(border_left_width(&document, a), 3.0); }
#[test] fn is_border_box_reports_the_box_sizing_keyword() { let html = "<!doctype html><style>#a{box-sizing:border-box}</style><div id=a></div><div id=b></div>"; let document = Document::parse(html); assert!(is_border_box(&document, find(&document, "a"))); assert!( !is_border_box(&document, find(&document, "b")), "box-sizing's initial computed value is content-box" ); }
#[test] fn display_reports_the_keywords_box_construction_branches_on() { let html = "<!doctype html><style>#a{display:none}#b{display:flow-root}\ #c{display:inline-block}#d{display:block}#e{display:inline}\ #f{display:table}#g{display:table-row}#h{display:table-cell}\ #i{display:inline-table}#j{display:table-row-group}</style>\ <div id=a></div><div id=b></div><div id=c></div><div id=d></div>\ <div id=e></div><div id=f></div><div id=g></div><div id=h></div>\ <div id=i></div><div id=j></div>"; let document = Document::parse(html); assert_eq!(display(&document, find(&document, "a")), Display::None); assert_eq!(display(&document, find(&document, "b")), Display::FlowRoot); assert_eq!(display(&document, find(&document, "c")), Display::InlineBlock); assert_eq!(display(&document, find(&document, "d")), Display::Block); assert_eq!(display(&document, find(&document, "e")), Display::Other, "inline is out of scope"); assert_eq!(display(&document, find(&document, "f")), Display::Table); assert_eq!(display(&document, find(&document, "g")), Display::TableRow); assert_eq!(display(&document, find(&document, "h")), Display::TableCell); assert_eq!( display(&document, find(&document, "i")), Display::Other, "inline-table needs a shrink-to-fit width and is out of scope" ); assert_eq!(display(&document, find(&document, "j")), Display::Other, "row groups are out of scope"); }
#[test] fn table_layout_reports_the_two_algorithms_and_defaults_to_auto() { let html = "<!doctype html><style>#a{table-layout:fixed}#b{table-layout:auto}</style>\ <div id=a></div><div id=b></div><div id=c></div>"; let document = Document::parse(html); assert_eq!(table_layout(&document, find(&document, "a")), TableLayout::Fixed); assert_eq!(table_layout(&document, find(&document, "b")), TableLayout::Auto); assert_eq!(table_layout(&document, find(&document, "c")), TableLayout::Auto, "auto is the initial value"); }
#[test] fn border_spacing_reports_both_axes_and_inherits() { let html = "<!doctype html><style>#a{border-spacing:4px 9px}#b{border-spacing:6px}\ #c{border-spacing:3px 7px}</style>\ <div id=a></div><div id=b></div><div id=c><div id=d></div></div><div id=e></div>"; let document = Document::parse(html); assert_eq!(border_spacing(&document, find(&document, "a")), (4.0, 9.0)); assert_eq!(border_spacing(&document, find(&document, "b")), (6.0, 6.0), "one value applies to both axes"); assert_eq!( border_spacing(&document, find(&document, "d")), (3.0, 7.0), "border-spacing is inherited, so a cell reads its table's value" ); assert_eq!(border_spacing(&document, find(&document, "e")), (0.0, 0.0), "zero is the initial value"); }
/// The two older accessors keep answering exactly as they did; [`display`] is an /// addition beside them, not a replacement (its own doc comment). #[test] fn display_agrees_with_the_two_boolean_accessors_it_was_added_beside() { let html = "<!doctype html><style>#a{display:none}#b{display:flow-root}\ #c{display:inline-block}</style>\ <div id=a></div><div id=b></div><div id=c></div>"; let document = Document::parse(html); for id in ["a", "b", "c"] { let node = find(&document, id); assert_eq!(is_display_none(&document, node), display(&document, node) == Display::None); assert_eq!(is_display_flow_root(&document, node), display(&document, node) == Display::FlowRoot); } }
#[test] fn only_inline_block_is_inline_level() { assert!(Display::InlineBlock.is_inline_level()); assert!(!Display::Block.is_inline_level()); assert!(!Display::FlowRoot.is_inline_level()); assert!(!Display::None.is_inline_level()); assert!(!Display::Other.is_inline_level()); }
#[test] fn vertical_align_reports_the_css21_keywords_and_lengths() { let html = "<!doctype html><style>#a{vertical-align:baseline}#b{vertical-align:top}\ #c{vertical-align:bottom}#d{vertical-align:middle}#e{vertical-align:12px}\ #f{vertical-align:-4px}#g{vertical-align:50%}#h{vertical-align:sub}</style>\ <div id=a></div><div id=b></div><div id=c></div><div id=d></div>\ <div id=e></div><div id=f></div><div id=g></div><div id=h></div>"; let document = Document::parse(html); let va = |id| vertical_align(&document, find(&document, id)); assert_eq!(va("a"), VerticalAlign::Baseline); assert_eq!(va("b"), VerticalAlign::Top); assert_eq!(va("c"), VerticalAlign::Bottom); assert_eq!(va("d"), VerticalAlign::Middle); assert_eq!(va("e"), VerticalAlign::Length(LengthPercentage::Length(12.0))); assert_eq!(va("f"), VerticalAlign::Length(LengthPercentage::Length(-4.0)), "negative lowers the box"); assert_eq!(va("g"), VerticalAlign::Length(LengthPercentage::Percentage(0.5))); assert_eq!(va("h"), VerticalAlign::Other, "sub needs font metrics and is out of scope"); }
#[test] fn vertical_align_defaults_to_baseline() { let document = Document::parse("<!doctype html><div id=a></div>"); assert_eq!(vertical_align(&document, find(&document, "a")), VerticalAlign::Baseline); }
#[test] fn line_height_is_a_length_unless_it_is_normal() { let html = "<!doctype html><style>#a{line-height:20px}#b{line-height:normal}\ #c{font-size:10px;line-height:1.5}#d{font-size:0;line-height:24px}</style>\ <div id=a></div><div id=b></div><div id=c></div><div id=d></div>"; let document = Document::parse(html); assert_eq!(line_height(&document, find(&document, "a")), Some(20.0)); assert_eq!(line_height(&document, find(&document, "b")), None, "normal reads the font"); assert_eq!(line_height(&document, find(&document, "c")), Some(15.0), "a number multiplies font-size"); assert_eq!( line_height(&document, find(&document, "d")), Some(24.0), "an explicit length is independent of a zero font-size, which is what the corpus relies on" ); }
#[test] fn text_align_narrows_to_the_three_values_a_line_box_distributes_by() { let html = "<!doctype html><style>#a{text-align:left}#b{text-align:right}\ #c{text-align:center}#d{text-align:justify}</style>\ <div id=a></div><div id=b></div><div id=c></div><div id=d></div><div id=e></div>"; let document = Document::parse(html); assert_eq!(text_align(&document, find(&document, "a")), TextAlign::Left); assert_eq!(text_align(&document, find(&document, "b")), TextAlign::Right); assert_eq!(text_align(&document, find(&document, "c")), TextAlign::Center); assert_eq!(text_align(&document, find(&document, "d")), TextAlign::Other, "justify is out of scope"); assert_eq!(text_align(&document, find(&document, "e")), TextAlign::Left, "start, in a ltr corpus"); }
#[test] fn overflow_accessors_report_the_computed_keyword_and_default_to_visible() { let html = "<!doctype html><style>#a{overflow-x:hidden;overflow-y:scroll}</style>\ <div id=a></div><div id=b></div>"; let document = Document::parse(html); let a = find(&document, "a"); let b = find(&document, "b"); assert_eq!(overflow_x(&document, a), "hidden"); assert_eq!(overflow_y(&document, a), "scroll"); assert_eq!(overflow_x(&document, b), "visible"); assert_eq!(overflow_y(&document, b), "visible"); }}