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();