diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index 7a37147..71402d6 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -13,7 +13,8 @@ use we_style::computed::{ BorderCollapse, BorderStyle, BoxShadow, BoxSizing, Clear, ComputedStyle, Cursor, Display, FlexDirection, FlexWrap, Float, FontStyle, GridAutoFlow, GridPlacement, GridTrackSize, JustifyContent, JustifyItems, JustifySelf, LengthOrAuto, Overflow, Position, StyledNode, - TextAlign, TextDecoration, TextOverflow, Transform, Visibility, WhiteSpace, WillChange, + TextAlign, TextDecoration, TextOverflow, TextTransform, Transform, Visibility, WhiteSpace, + WillChange, }; use we_text::font::Font; @@ -1516,12 +1517,39 @@ fn build_box( } collapsed }; + let text = apply_text_transform(&text, style.text_transform); Some(LayoutBox::new(BoxType::TextRun { node, text }, style)) } NodeData::Comment { .. } => None, } } +fn apply_text_transform(text: &str, transform: TextTransform) -> String { + match transform { + TextTransform::None => text.to_string(), + TextTransform::Uppercase => text.chars().flat_map(char::to_uppercase).collect(), + TextTransform::Lowercase => text.chars().flat_map(char::to_lowercase).collect(), + TextTransform::Capitalize => { + let mut out = String::new(); + let mut at_word_start = true; + for ch in text.chars() { + if ch.is_alphanumeric() { + if at_word_start { + out.extend(ch.to_uppercase()); + at_word_start = false; + } else { + out.push(ch); + } + } else { + out.push(ch); + at_word_start = true; + } + } + out + } + } +} + /// Collapse runs of whitespace to a single space. fn collapse_whitespace(s: &str) -> String { let mut result = String::new(); @@ -9774,6 +9802,50 @@ body { margin: 0; } ); } + #[test] + fn text_transform_uppercase_applies_to_layout_text_run() { + let html_str = r#" + +
Dagens bild
"#; + let doc = we_html::parse_html(html_str); + fn contains_source_text(doc: &Document, node: NodeId, want: &str) -> bool { + if let NodeData::Text { data } = doc.node_data(node) { + if data == want { + return true; + } + } + doc.children(node) + .any(|child| contains_source_text(doc, child, want)) + } + + let font = test_font(); + let sheets = extract_stylesheets(&doc); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); + let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); + + fn find_text<'a>(b: &'a LayoutBox, want: &str) -> Option<&'a LayoutBox> { + if let BoxType::TextRun { text, .. } = &b.box_type { + if text == want { + return Some(b); + } + } + for c in &b.children { + if let Some(found) = find_text(c, want) { + return Some(found); + } + } + None + } + + assert!(contains_source_text(&doc, doc.root(), "Dagens bild")); + assert!( + find_text(&tree.root, "DAGENS BILD").is_some(), + "layout should apply inherited text-transform to text runs" + ); + } + #[test] fn flex_align_items_uses_max_height_clamped_cross_size() { let html_str = r#" diff --git a/crates/style/src/computed.rs b/crates/style/src/computed.rs index 61a44f8..231cf28 100644 --- a/crates/style/src/computed.rs +++ b/crates/style/src/computed.rs @@ -239,6 +239,19 @@ pub enum TextDecoration { LineThrough, } +// --------------------------------------------------------------------------- +// TextTransform +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum TextTransform { + #[default] + None, + Uppercase, + Lowercase, + Capitalize, +} + // --------------------------------------------------------------------------- // BorderCollapse // --------------------------------------------------------------------------- @@ -806,6 +819,7 @@ pub struct ComputedStyle { pub font_family: Atom, pub text_align: TextAlign, pub text_decoration: TextDecoration, + pub text_transform: TextTransform, pub line_height: f32, pub white_space: WhiteSpace, @@ -957,6 +971,7 @@ impl Default for ComputedStyle { font_family: Atom::new(""), text_align: TextAlign::Left, text_decoration: TextDecoration::None, + text_transform: TextTransform::None, line_height: 19.2, // 1.2 * 16 white_space: WhiteSpace::Normal, @@ -1085,6 +1100,7 @@ fn is_inherited_property(property: &str) -> bool { | "font-family" | "text-align" | "text-decoration" + | "text-transform" | "line-height" | "visibility" | "white-space" @@ -2200,6 +2216,20 @@ fn apply_property( }; } + // Text-transform (inherited) + "text-transform" => { + style.text_transform = match value { + CssValue::Keyword(k) => match k.as_str() { + "uppercase" => TextTransform::Uppercase, + "lowercase" => TextTransform::Lowercase, + "capitalize" => TextTransform::Capitalize, + _ => style.text_transform, + }, + CssValue::None => TextTransform::None, + _ => style.text_transform, + }; + } + // Line-height (inherited) "line-height" => match value { CssValue::Keyword(k) if k == "normal" => { @@ -3156,6 +3186,7 @@ fn inherit_property(style: &mut ComputedStyle, property: &str, parent: &Computed "font-family" => style.font_family = parent.font_family.clone(), "text-align" => style.text_align = parent.text_align, "text-decoration" => style.text_decoration = parent.text_decoration, + "text-transform" => style.text_transform = parent.text_transform, "line-height" => style.line_height = parent.line_height, "visibility" => style.visibility = parent.visibility, "white-space" => style.white_space = parent.white_space, @@ -3274,6 +3305,7 @@ fn reset_property_to_initial(style: &mut ComputedStyle, property: &str) { "font-family" => style.font_family = initial.font_family.clone(), "text-align" => style.text_align = initial.text_align, "text-decoration" => style.text_decoration = initial.text_decoration, + "text-transform" => style.text_transform = initial.text_transform, "line-height" => style.line_height = initial.line_height, "white-space" => style.white_space = initial.white_space, "background-color" => style.background_color = initial.background_color, @@ -3775,6 +3807,7 @@ fn compute_style_for_element_with_inputs( font_family: parent_style.font_family.clone(), text_align: parent_style.text_align, text_decoration: parent_style.text_decoration, + text_transform: parent_style.text_transform, line_height: parent_style.line_height, white_space: parent_style.white_space, visibility: parent_style.visibility, @@ -4489,6 +4522,28 @@ mod tests { assert_eq!(p_node.style.font_size, 20.0); } + #[test] + fn text_transform_inherits_and_can_reset() { + let (mut doc, _, _, body) = make_doc_with_body(); + let div = doc.create_element("div"); + let span = doc.create_element("span"); + let em = doc.create_element("em"); + doc.append_child(body, div); + doc.append_child(div, span); + doc.append_child(div, em); + + let ss = Parser::parse("div { text-transform: uppercase; } em { text-transform: none; }"); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); + let body_node = &styled.children[0]; + let div_node = &body_node.children[0]; + let span_node = &div_node.children[0]; + let em_node = &div_node.children[1]; + + assert_eq!(div_node.style.text_transform, TextTransform::Uppercase); + assert_eq!(span_node.style.text_transform, TextTransform::Uppercase); + assert_eq!(em_node.style.text_transform, TextTransform::None); + } + #[test] fn margin_does_not_inherit() { let (mut doc, _, _, body) = make_doc_with_body();