on master help sign in
CSS values mean what CSS says

line-height and content pass through as written: a bare line-height number is a multiplier (it was px from 2 up), and content takes CSS quotes (they were added). The only value rule left is that a bare number is px for lengths.

commita54158003c2c22973bb1895dd4c111a71c95e3c5 authoriko <mail@iko.soy> dateSat 26 Sep 2026, 03:55 (2 weeks ago) parent0234fb8
5 files changed+16−47
crates/htmlang-core/src/codegen.rs−41 crates/htmlang-core/src/vocab.rs+5−3 examples/demo.hl+1−1 tests/regressions.rs+8 tests/snapshots.rs+2−2
crates/htmlang-core/src/codegen.rs−41
@@ -1700,11 +1700,6 @@ fn attrs_to_css(
17001700 },
17011701
17021702 // Typography
1703− "line-height" => {
1704− if let Some(v) = val {
1705− push_css(&mut css, "line-height", &css_line_height(v));
1706− }
1707− }
17081703 "letter-spacing" => {
17091704 if let Some(v) = val {
17101705 push_css(&mut css, "letter-spacing", &css_px(v));
@@ -1987,23 +1982,6 @@ fn attrs_to_css(
19871982 }
19881983 }
19891984
1990− "content" => {
1991− if let Some(v) = val {
1992− // Wrap in quotes if not already quoted and not a CSS keyword
1993− if v.starts_with('"')
1994− || v.starts_with('\'')
1995− || v == "none"
1996− || v == "normal"
1997− || v.starts_with("attr(")
1998− || v.starts_with("counter(")
1999− {
2000− push_css(&mut css, "content", v);
2001− } else {
2002− push_css(&mut css, "content", &format!("\"{}\"", v));
2003− }
2004− }
2005− }
2006−
20071985 // --- CSS Shorthands ---
20081986 "line-clamp" => {
20091987 if let Some(v) = val {
@@ -2071,25 +2049,6 @@ fn css_px(value: &str) -> String {
20712049 }
20722050}
20732051
2074−/// Format a `line-height` value. CSS accepts either a unitless multiplier
2075−/// (e.g. `1.5`) or a length (e.g. `24px`). Plain integers in htmlang source
2076−/// are ambiguous: `[line-height 24]` was historically emitted as `24` (which
2077−/// CSS interprets as 24× font-size — almost never what anyone wants). Treat
2078−/// integers ≥ 2 as pixel lengths; anything with a decimal, an existing unit,
2079−/// or the value `0`/`1` passes through unchanged.
2080−fn css_line_height(value: &str) -> String {
2081− let v = value.trim();
2082− if v == "0" || v == "1" {
2083− return v.to_string();
2084− }
2085− // Plain integer — treat as pixel length. Decimals, units, keywords and
2086− // functions pass through.
2087− if !v.is_empty() && v.chars().all(|c| c.is_ascii_digit()) {
2088− return format!("{}px", v);
2089− }
2090− v.to_string()
2091−}
2092−
20932052/// Format multiple space-separated values, each getting px if needed.
20942053fn css_px_multi(value: &str) -> String {
20952054 value
crates/htmlang-core/src/vocab.rs+5−3
@@ -155,9 +155,11 @@ pub fn is_length_property(name: &str) -> bool {
155155 "border", "border-top", "border-right", "border-bottom", "border-left", "border-block",
156156 "border-inline", "outline", "column-rule",
157157 ];
158− EXACT.contains(&name)
159− || PREFIXES.iter().any(|p| name.starts_with(p))
160− || SUFFIXES.iter().any(|s| name.ends_with(s))
158+ // A bare `line-height` number is a multiplier, not a length
159+ name != "line-height"
160+ && (EXACT.contains(&name)
161+ || PREFIXES.iter().any(|p| name.starts_with(p))
162+ || SUFFIXES.iter().any(|s| name.ends_with(s)))
161163}
162164
163165/// State prefixes and the selector each adds: `hover:color red` styles
examples/demo.hl+1−1
@@ -163,7 +163,7 @@
163163 @el [
164164 width 24, height 24, border-radius 4,
165165 background $primary, color white,
166− text-align center, font-size 12, font-weight bold, line-height 24
166+ text-align center, font-size 12, font-weight bold, line-height 24px
167167 ]
168168 $i
169169
tests/regressions.rs+8
@@ -455,3 +455,11 @@ fn links_and_images_are_output_as_written() {
455455 assert!(!out.contains("Skip to content"), "{}", out);
456456 assert!(!out.contains("hl-main"), "{}", out);
457457}
458+
459+#[test]
460+fn css_values_pass_through() {
461+ let out = compile("@el [line-height 24, before:content \"→ \", after:content attr(title)] x");
462+ assert!(out.contains("line-height:24;"), "{}", out);
463+ assert!(out.contains("content:\"→ \""), "{}", out);
464+ assert!(out.contains("content:attr(title)"), "{}", out);
465+}
tests/snapshots.rs+2−2
@@ -2225,7 +2225,7 @@ fn each_else_non_empty_list() {
22252225
22262226#[test]
22272227fn pseudo_element_before_content() {
2228− let output = compile("@el [before:content arrow, before:color red]\n Hello");
2228+ let output = compile("@el [before:content \"arrow\", before:color red]\n Hello");
22292229 assert!(
22302230 output.contains("::before"),
22312231 "should generate ::before CSS: {}",
@@ -2245,7 +2245,7 @@ fn pseudo_element_before_content() {
22452245
22462246#[test]
22472247fn pseudo_element_after_content() {
2248− let output = compile("@el [after:content ✓]\n Done");
2248+ let output = compile("@el [after:content \"✓\"]\n Done");
22492249 assert!(
22502250 output.contains("::after"),
22512251 "should generate ::after CSS: {}",