css: share the axis shorthand table with the CSSOM

The cascade expanded overscroll-behavior into longhands but
CSSStyleDeclaration didn't, so setting the shorthand left
overscrollBehaviorX reading empty and a style= block round-tripped
through the object lost it.

CssParser.axis_shorthands is now the one list, with axisShorthand and
axisLonghand as the lookups both sides use: the CSSOM's overflow-only
special cases (set, apply, remove, priority, serialize) became that
lookup, and OverflowPair became AxisPair. Verified against Chrome 153:
`overscroll-behavior: contain auto` reads back per axis, collapses to
`contain` when both match, and serializes as one declaration.
This commit is contained in:
Adrià Arrufat committed 2026-09-22 16:39:13 +02:00
1 parent fdee7d558c
commit 93c551bed6
4 files changed
+107 -56

No files matched your search

+5 -13
View File
@@ -1303,12 +1303,6 @@ fn foldDeclarations(block: []const u8, customs: ?*CustomSink) !TrackedProperties
return slots.props();
}
// The `<x> [<y>]` shorthands the cascade expands into the tracked longhands.
const axis_shorthands = [_]struct { name: []const u8, x: []const u8, y: []const u8 }{
.{ .name = "overflow", .x = "overflow-x", .y = "overflow-y" },
.{ .name = "overscroll-behavior", .x = "overscroll-behavior-x", .y = "overscroll-behavior-y" },
};
/// One block's winning value per tracked property, folded in declaration
/// order.
const Slots = struct {
@@ -1332,13 +1326,11 @@ const Slots = struct {
slots: [property_names.len]Slot = @splat(.{}),
fn apply(self: *Slots, name: []const u8, value: []const u8, important: bool) void {
for (axis_shorthands) |shorthand| {
if (std.ascii.eqlIgnoreCase(name, shorthand.name)) {
const values = CssParser.splitAxisPair(value) orelse return;
self.apply(shorthand.x, values.x, important);
self.apply(shorthand.y, values.y, important);
return;
}
if (CssParser.axisShorthand(name)) |shorthand| {
const values = CssParser.splitAxisPair(value) orelse return;
self.apply(shorthand.x, values.x, important);
self.apply(shorthand.y, values.y, important);
return;
}
for (property_names, &self.slots) |tracked, *slot| {
if (std.ascii.eqlIgnoreCase(name, tracked)) {
+47
View File
@@ -27,6 +27,53 @@ pub const Declaration = struct {
pub const AxisPair = struct { x: []const u8, y: []const u8 };
pub const AxisShorthand = struct {
name: []const u8,
x: []const u8,
y: []const u8,
};
// The `<x> [<y>]` shorthands whose longhands the style cascade tracks. Both the
// CSSOM object and the cascade store these expanded: setting one sets both
// longhands, reading or serializing it recombines them.
pub const axis_shorthands = [_]AxisShorthand{
.{ .name = "overflow", .x = "overflow-x", .y = "overflow-y" },
.{ .name = "overscroll-behavior", .x = "overscroll-behavior-x", .y = "overscroll-behavior-y" },
};
/// The axis shorthand `name` names, if it names one.
pub fn axisShorthand(name: []const u8) ?AxisShorthand {
for (axis_shorthands) |shorthand| {
if (std.ascii.eqlIgnoreCase(name, shorthand.name)) {
return shorthand;
}
}
return null;
}
pub const AxisLonghand = struct {
shorthand: AxisShorthand,
is_x: bool,
/// The longhand on the other axis.
pub fn partner(self: AxisLonghand) []const u8 {
return if (self.is_x) self.shorthand.y else self.shorthand.x;
}
};
/// The axis shorthand `name` is a longhand of, if it is one.
pub fn axisLonghand(name: []const u8) ?AxisLonghand {
for (axis_shorthands) |shorthand| {
if (std.ascii.eqlIgnoreCase(name, shorthand.x)) {
return .{ .shorthand = shorthand, .is_x = true };
}
if (std.ascii.eqlIgnoreCase(name, shorthand.y)) {
return .{ .shorthand = shorthand, .is_x = false };
}
}
return null;
}
/// An `<x> [<y>]` axis shorthand such as `overflow` or `overscroll-behavior`;
/// a single value applies to both axes. More than two values is invalid and
/// null, as is an empty declaration.
+19
View File
@@ -113,6 +113,25 @@
}
</script>
<script id="overscrollBehaviorShorthand">
{
const div = document.createElement('div');
div.style.cssText = 'overscroll-behavior: contain auto';
testing.expectEqual('contain', div.style.overscrollBehaviorX);
testing.expectEqual('auto', div.style.overscrollBehaviorY);
testing.expectEqual('contain auto', div.style.overscrollBehavior);
testing.expectEqual(2, div.style.length);
div.style.overscrollBehaviorY = 'contain';
testing.expectEqual('contain', div.style.overscrollBehavior);
testing.expectEqual('overscroll-behavior: contain;', div.style.cssText);
div.style.setProperty('overscroll-behavior', 'none');
testing.expectEqual('none', div.style.overscrollBehaviorX);
testing.expectEqual('none', div.style.overscrollBehaviorY);
}
</script>
<script id="overflowShorthand">
{
const div = document.createElement('div');
+36 -43
View File
@@ -136,13 +136,13 @@ pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const
return getDefaultPropertyValue(self, wrapped);
}
/// The value of a declared property, or null when it isn't declared. The
/// `overflow` shorthand reads as its longhands when both are present with
/// the same priority, the way the CSSOM serializes a shorthand.
/// The value of a declared property, or null when it isn't declared. An axis
/// shorthand reads as its longhands when both are present with the same
/// priority, the way the CSSOM serializes a shorthand.
pub fn declaredValue(self: *const CSSStyleDeclaration, name: String, frame: *Frame) ?[]const u8 {
if (name.eql(overflow_shorthand)) {
const x = self.findProperty(comptime .wrap("overflow-x")) orelse return null;
const pair = self.overflowPair(x) orelse return null;
if (CssParser.axisShorthand(name.str())) |shorthand| {
const x = self.findProperty(.wrap(shorthand.x)) orelse return null;
const pair = self.axisPair(x) orelse return null;
var buf = std.Io.Writer.Allocating.init(frame.local_arena);
pair.formatValue(&buf.writer) catch return null;
return buf.written();
@@ -151,19 +151,14 @@ pub fn declaredValue(self: *const CSSStyleDeclaration, name: String, frame: *Fra
return prop._value.str();
}
// `overflow` is the one shorthand whose longhands the style cascade folds
// (StyleManager tracks overflow-x and overflow-y), so it is the one this
// object stores expanded, as the CSSOM does for every shorthand: setting it
// sets both longhands, reading or serializing it recombines them.
const overflow_shorthand: String = .wrap("overflow");
/// Both overflow longhands, declared with the same priority: the pair reads
/// and serializes as the shorthand.
const OverflowPair = struct {
/// Both longhands of an axis shorthand, declared with the same priority: the
/// pair reads and serializes as the shorthand.
const AxisPair = struct {
name: []const u8,
x: *const Property,
y: *const Property,
fn formatValue(self: OverflowPair, writer: *std.Io.Writer) !void {
fn formatValue(self: AxisPair, writer: *std.Io.Writer) !void {
try self.x._value.format(writer);
if (!self.x._value.eql(self.y._value)) {
try writer.writeByte(' ');
@@ -171,26 +166,24 @@ const OverflowPair = struct {
}
}
fn format(self: OverflowPair, writer: *std.Io.Writer) !void {
try writer.writeAll("overflow: ");
fn format(self: AxisPair, writer: *std.Io.Writer) !void {
try writer.writeAll(self.name);
try writer.writeAll(": ");
try self.formatValue(writer);
try formatDeclarationEnd(self.x._important, writer);
}
};
/// The pair `prop` belongs to, when it is an overflow longhand and the other
/// is declared with the same priority.
fn overflowPair(self: *const CSSStyleDeclaration, prop: *const Property) ?OverflowPair {
const is_x = prop._name.eql(comptime .wrap("overflow-x"));
if (!is_x and !prop._name.eql(comptime .wrap("overflow-y"))) {
return null;
}
const other: String = if (is_x) comptime .wrap("overflow-y") else comptime .wrap("overflow-x");
const partner = self.findProperty(other) orelse return null;
/// The pair `prop` belongs to, when it is an axis longhand and the other is
/// declared with the same priority.
fn axisPair(self: *const CSSStyleDeclaration, prop: *const Property) ?AxisPair {
const longhand = CssParser.axisLonghand(prop._name.str()) orelse return null;
const partner = self.findProperty(.wrap(longhand.partner())) orelse return null;
if (partner._important != prop._important) {
return null;
}
return if (is_x) .{ .x = prop, .y = partner } else .{ .x = partner, .y = prop };
const name = longhand.shorthand.name;
return if (longhand.is_x) .{ .name = name, .x = prop, .y = partner } else .{ .name = name, .x = partner, .y = prop };
}
fn resolvedDimension(element: *Element, dimension: enum { width, height }, frame: *Frame) []const u8 {
@@ -207,9 +200,9 @@ fn resolvedDimension(element: *Element, dimension: enum { width, height }, frame
pub fn getPropertyPriority(self: *const CSSStyleDeclaration, property_name: []const u8, frame: *Frame) []const u8 {
const normalized = normalizePropertyName(property_name, &frame.buf);
const wrapped = String.wrap(normalized);
if (wrapped.eql(overflow_shorthand)) {
const x = self.findProperty(comptime .wrap("overflow-x")) orelse return "";
const pair = self.overflowPair(x) orelse return "";
if (CssParser.axisShorthand(normalized)) |shorthand| {
const x = self.findProperty(.wrap(shorthand.x)) orelse return "";
const pair = self.axisPair(x) orelse return "";
return if (pair.x._important) "important" else "";
}
const prop = self.findProperty(wrapped) orelse return "";
@@ -240,10 +233,10 @@ pub fn setProperty(self: *CSSStyleDeclaration, property_name: []const u8, value:
/// not override an earlier !important one (CSS cascade precedence).
fn applyParsedDeclaration(self: *CSSStyleDeclaration, declaration: CssParser.Declaration, frame: *Frame) !void {
const normalized = normalizePropertyName(declaration.name, &frame.buf);
if (overflow_shorthand.eqlSlice(normalized)) {
if (CssParser.axisShorthand(normalized)) |shorthand| {
const values = CssParser.splitAxisPair(declaration.value) orelse return;
try self.applyParsedDeclaration(.{ .name = "overflow-x", .value = values.x, .important = declaration.important }, frame);
try self.applyParsedDeclaration(.{ .name = "overflow-y", .value = values.y, .important = declaration.important }, frame);
try self.applyParsedDeclaration(.{ .name = shorthand.x, .value = values.x, .important = declaration.important }, frame);
try self.applyParsedDeclaration(.{ .name = shorthand.y, .value = values.y, .important = declaration.important }, frame);
return;
}
if (!declaration.important) {
@@ -266,10 +259,10 @@ fn setPropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, value:
}
const normalized = normalizePropertyName(property_name, &frame.buf);
if (overflow_shorthand.eqlSlice(normalized)) {
if (CssParser.axisShorthand(normalized)) |shorthand| {
const values = CssParser.splitAxisPair(value) orelse return false;
const x = try self.setPropertyImpl("overflow-x", values.x, important, frame);
const y = try self.setPropertyImpl("overflow-y", values.y, important, frame);
const x = try self.setPropertyImpl(shorthand.x, values.x, important, frame);
const y = try self.setPropertyImpl(shorthand.y, values.y, important, frame);
return x or y;
}
@@ -309,10 +302,10 @@ pub fn removeProperty(self: *CSSStyleDeclaration, property_name: []const u8, fra
fn removePropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, frame: *Frame) !?[]const u8 {
const normalized = normalizePropertyName(property_name, &frame.buf);
if (overflow_shorthand.eqlSlice(normalized)) {
const old_value = self.declaredValue(overflow_shorthand, frame) orelse "";
const x = try self.removePropertyImpl("overflow-x", frame);
const y = try self.removePropertyImpl("overflow-y", frame);
if (CssParser.axisShorthand(normalized)) |shorthand| {
const old_value = self.declaredValue(.wrap(shorthand.name), frame) orelse "";
const x = try self.removePropertyImpl(shorthand.x, frame);
const y = try self.removePropertyImpl(shorthand.y, frame);
if (x == null and y == null) {
return null;
}
@@ -402,7 +395,7 @@ pub fn replaceCssText(self: *CSSStyleDeclaration, text: []const u8, frame: *Fram
pub fn format(self: *const CSSStyleDeclaration, writer: *std.Io.Writer) !void {
var first = true;
// An overflow pair serializes once, where its first longhand sits.
// An axis pair serializes once, where its first longhand sits.
var skip: ?*const Property = null;
var it = self.iterator();
while (it.next()) |prop| {
@@ -413,7 +406,7 @@ pub fn format(self: *const CSSStyleDeclaration, writer: *std.Io.Writer) !void {
try writer.writeByte(' ');
}
first = false;
if (self.overflowPair(prop)) |pair| {
if (self.axisPair(prop)) |pair| {
try pair.format(writer);
skip = if (pair.x == prop) pair.y else pair.x;
} else {