Merge pull request #2823 from lightpanda-io/css_normalize_leading_zero

css: normalize fractions without leading 0
This commit is contained in:
Karl Seguin
2026-06-27 12:07:50 +08:00
committed by GitHub
2 changed files with 78 additions and 2 deletions

View File

@@ -159,6 +159,35 @@
<div id=crash1 style="background-position: 5% .1em"></div>
<script id="crash_case_1">
{
testing.expectEqual('5% .1em', $('#crash1').style.backgroundPosition);
// Per CSSOM, bare ".1em" serializes with a leading zero -> "0.1em"
testing.expectEqual('5% 0.1em', $('#crash1').style.backgroundPosition);
}
</script>
<div id=opacity-detect style="opacity:.55"></div>
<script id="leadingZeroNormalization">
{
// jQuery's support.opacity feature-detect reads an inline "opacity:.55"
// back and matches it against /^0.55$/; bare ".55" must serialize as "0.55".
testing.expectEqual('0.55', $('#opacity-detect').style.opacity);
const div = $('#test-div');
div.style.cssText = '';
// leading-dot value via property assignment
div.style.opacity = '.5';
testing.expectEqual('0.5', div.style.opacity);
// negative leading-dot, with unit, preserved otherwise
div.style.marginLeft = '-.5px';
testing.expectEqual('-0.5px', div.style.marginLeft);
// existing leading digit is untouched
div.style.lineHeight = '1.5';
testing.expectEqual('1.5', div.style.lineHeight);
// multiple numbers in one value
div.style.margin = '.5em .25em';
testing.expectEqual('0.5em 0.25em', div.style.margin);
}
</script>

View File

@@ -245,7 +245,9 @@ fn normalizePropertyName(name: []const u8, buf: []u8) []const u8 {
}
// Normalize CSS property values for canonical serialization
fn normalizePropertyValue(arena: Allocator, property_name: []const u8, value: []const u8) ![]const u8 {
fn normalizePropertyValue(arena: Allocator, property_name: []const u8, raw_value: []const u8) ![]const u8 {
const value = try normalizeLeadingZeros(arena, raw_value);
// Per CSSOM spec, unitless zero in length properties should serialize as "0px"
if (std.mem.eql(u8, value, "0") and isLengthProperty(property_name)) {
return "0px";
@@ -286,6 +288,51 @@ fn normalizePropertyValue(arena: Allocator, property_name: []const u8, value: []
return value;
}
// Insert a leading "0" before any bare ".<digit>". A value might have multiple
// such bare digits
fn normalizeLeadingZeros(arena: Allocator, value: []const u8) ![]const u8 {
// A bare ".<digit>" needs at least 2 chars, and a trailing "." can never be
// followed by a digit. So we only ever scan up to value.len - 1, which lets
// us index value[i + 1] without a bounds check.
if (value.len < 2) {
return value;
}
var inserts: usize = 0;
for (value[0 .. value.len - 1], 0..) |c, i| {
if (c != '.') {
continue;
}
if (!std.ascii.isDigit(value[i + 1])) {
// next value isn't a digit
continue;
}
if (i > 0 and std.ascii.isDigit(value[i - 1])) {
// previous value is a digit
continue;
}
inserts += 1;
}
if (inserts == 0) {
return value;
}
const buf = try arena.alloc(u8, value.len + inserts);
var w: usize = 0;
for (value[0 .. value.len - 1], 0..) |c, i| {
if (c == '.' and std.ascii.isDigit(value[i + 1]) and (i == 0 or !std.ascii.isDigit(value[i - 1]))) {
buf[w] = '0';
w += 1;
}
buf[w] = c;
w += 1;
}
// The last char is copied unconditionally: it can't start a bare ".<digit>".
buf[w] = value[value.len - 1];
w += 1;
return buf[0..w];
}
// Canonicalize anchor-size() so that the dashed ident (anchor name) comes before the size keyword.
// e.g. "anchor-size(width --foo)" -> "anchor-size(--foo width)"
fn canonicalizeAnchorSize(arena: Allocator, value: []const u8, start_index: usize) ![]const u8 {