mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 19:08:50 +00:00
430 lines
17 KiB
Zig
430 lines
17 KiB
Zig
pub const css = @import("../css_parser.zig");
|
|
const Result = css.Result;
|
|
const Printer = css.Printer;
|
|
const PrintErr = css.PrintErr;
|
|
const LengthPercentage = css.css_values.length.LengthPercentage;
|
|
const Percentage = css.css_values.percentage.Percentage;
|
|
|
|
/// A CSS `<position>` value,
|
|
/// as used in the `background-position` property, gradients, masks, etc.
|
|
pub const Position = struct {
|
|
/// The x-position.
|
|
x: HorizontalPosition,
|
|
/// The y-position.
|
|
y: VerticalPosition,
|
|
|
|
pub fn parse(input: *css.Parser) Result(Position) {
|
|
// Try parsing a horizontal position first
|
|
if (input.tryParse(HorizontalPosition.parse, .{}).asValue()) |horizontal_pos| {
|
|
switch (horizontal_pos) {
|
|
.center => {
|
|
// Try parsing a vertical position next
|
|
if (input.tryParse(VerticalPosition.parse, .{}).asValue()) |y| {
|
|
return .{ .result = Position{
|
|
.x = .center,
|
|
.y = y,
|
|
} };
|
|
}
|
|
|
|
// If it didn't work, assume the first actually represents a y position,
|
|
// and the next is an x position. e.g. `center left` rather than `left center`.
|
|
const x = input.tryParse(HorizontalPosition.parse, .{}).unwrapOr(HorizontalPosition.center);
|
|
const y = VerticalPosition.center;
|
|
return .{ .result = Position{ .x = x, .y = y } };
|
|
},
|
|
.length => |*x| {
|
|
// If we got a length as the first component, then the second must
|
|
// be a keyword or length (not a side offset).
|
|
if (input.tryParse(VerticalPositionKeyword.parse, .{}).asValue()) |y_keyword| {
|
|
const y = VerticalPosition{ .side = .{
|
|
.side = y_keyword,
|
|
.offset = null,
|
|
} };
|
|
return .{ .result = Position{ .x = .{ .length = x.* }, .y = y } };
|
|
}
|
|
if (input.tryParse(LengthPercentage.parse, .{}).asValue()) |y_lp| {
|
|
const y = VerticalPosition{ .length = y_lp };
|
|
return .{ .result = Position{ .x = .{ .length = x.* }, .y = y } };
|
|
}
|
|
const y = VerticalPosition.center;
|
|
_ = input.tryParse(css.Parser.expectIdentMatching, .{"center"});
|
|
return .{ .result = Position{ .x = .{ .length = x.* }, .y = y } };
|
|
},
|
|
.side => |*side| {
|
|
const x_keyword = side.side;
|
|
const lp = side.offset;
|
|
|
|
// If we got a horizontal side keyword (and optional offset), expect another for the vertical side.
|
|
// e.g. `left center` or `left 20px center`
|
|
if (input.tryParse(css.Parser.expectIdentMatching, .{"center"}).isOk()) {
|
|
const x = HorizontalPosition{ .side = .{
|
|
.side = x_keyword,
|
|
.offset = lp,
|
|
} };
|
|
const y = VerticalPosition.center;
|
|
return .{ .result = Position{ .x = x, .y = y } };
|
|
}
|
|
|
|
// e.g. `left top`, `left top 20px`, `left 20px top`, or `left 20px top 20px`
|
|
if (input.tryParse(VerticalPositionKeyword.parse, .{}).asValue()) |y_keyword| {
|
|
const y_lp = switch (input.tryParse(LengthPercentage.parse, .{})) {
|
|
.result => |vv| vv,
|
|
.err => null,
|
|
};
|
|
const x = HorizontalPosition{ .side = .{
|
|
.side = x_keyword,
|
|
.offset = lp,
|
|
} };
|
|
const y = VerticalPosition{ .side = .{
|
|
.side = y_keyword,
|
|
.offset = y_lp,
|
|
} };
|
|
return .{ .result = Position{ .x = x, .y = y } };
|
|
}
|
|
|
|
// If we didn't get a vertical side keyword (e.g. `left 20px`), then apply the offset to the vertical side.
|
|
const x = HorizontalPosition{ .side = .{
|
|
.side = x_keyword,
|
|
.offset = null,
|
|
} };
|
|
const y = if (lp) |lp_val|
|
|
VerticalPosition{ .length = lp_val }
|
|
else
|
|
VerticalPosition.center;
|
|
return .{ .result = Position{ .x = x, .y = y } };
|
|
},
|
|
}
|
|
}
|
|
|
|
// If the horizontal position didn't parse, then it must be out of order. Try vertical position keyword.
|
|
const y_keyword = switch (VerticalPositionKeyword.parse(input)) {
|
|
.result => |vv| vv,
|
|
.err => |e| return .{ .err = e },
|
|
};
|
|
const lp_and_x_pos = input.tryParse(struct {
|
|
fn parse(i: *css.Parser) Result(struct { ?LengthPercentage, HorizontalPosition }) {
|
|
const y_lp = i.tryParse(LengthPercentage.parse, .{}).asValue();
|
|
if (i.tryParse(HorizontalPositionKeyword.parse, .{}).asValue()) |x_keyword| {
|
|
const x_lp = i.tryParse(LengthPercentage.parse, .{}).asValue();
|
|
const x_pos = HorizontalPosition{ .side = .{
|
|
.side = x_keyword,
|
|
.offset = x_lp,
|
|
} };
|
|
return .{ .result = .{ y_lp, x_pos } };
|
|
}
|
|
if (i.expectIdentMatching("center").asErr()) |e| return .{ .err = e };
|
|
const x_pos = HorizontalPosition.center;
|
|
return .{ .result = .{ y_lp, x_pos } };
|
|
}
|
|
}.parse, .{});
|
|
|
|
if (lp_and_x_pos.asValue()) |tuple| {
|
|
const y_lp = tuple[0];
|
|
const x = tuple[1];
|
|
const y = VerticalPosition{ .side = .{
|
|
.side = y_keyword,
|
|
.offset = y_lp,
|
|
} };
|
|
return .{ .result = Position{ .x = x, .y = y } };
|
|
}
|
|
|
|
const x = HorizontalPosition.center;
|
|
const y = VerticalPosition{ .side = .{
|
|
.side = y_keyword,
|
|
.offset = null,
|
|
} };
|
|
return .{ .result = Position{ .x = x, .y = y } };
|
|
}
|
|
|
|
pub fn toCss(this: *const Position, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void {
|
|
if (this.x == .side and this.y == .length and this.x.side.side != .left) {
|
|
try this.x.toCss(W, dest);
|
|
try dest.writeStr(" top ");
|
|
try this.y.length.toCss(W, dest);
|
|
} else if (this.x == .side and this.x.side.side != .left and this.y.isCenter()) {
|
|
// If there is a side keyword with an offset, "center" must be a keyword not a percentage.
|
|
try this.x.toCss(W, dest);
|
|
try dest.writeStr(" center");
|
|
} else if (this.x == .length and this.y == .side and this.y.side.side != .top) {
|
|
try dest.writeStr("left ");
|
|
try this.x.length.toCss(W, dest);
|
|
try dest.writeStr(" ");
|
|
try this.y.toCss(W, dest);
|
|
} else if (this.x.isCenter() and this.y.isCenter()) {
|
|
// `center center` => 50%
|
|
try this.x.toCss(W, dest);
|
|
} else if (this.x == .length and this.y.isCenter()) {
|
|
// `center` is assumed if omitted.
|
|
try this.x.length.toCss(W, dest);
|
|
} else if (this.x == .side and this.x.side.offset == null and this.y.isCenter()) {
|
|
const p: LengthPercentage = this.x.side.side.intoLengthPercentage();
|
|
try p.toCss(W, dest);
|
|
} else if (this.y == .side and this.y.side.offset == null and this.x.isCenter()) {
|
|
try this.y.toCss(W, dest);
|
|
} else if (this.x == .side and this.x.side.offset == null and this.y == .side and this.y.side.offset == null) {
|
|
const x: LengthPercentage = this.x.side.side.intoLengthPercentage();
|
|
const y: LengthPercentage = this.y.side.side.intoLengthPercentage();
|
|
try x.toCss(W, dest);
|
|
try dest.writeStr(" ");
|
|
try y.toCss(W, dest);
|
|
} else {
|
|
const zero = LengthPercentage.zero();
|
|
const fifty = LengthPercentage{ .percentage = .{ .v = 0.5 } };
|
|
const x_len: ?*const LengthPercentage = x_len: {
|
|
switch (this.x) {
|
|
.side => |side| {
|
|
if (side.side == .left) {
|
|
if (side.offset) |*offset| {
|
|
if (offset.isZero()) {
|
|
break :x_len &zero;
|
|
} else {
|
|
break :x_len offset;
|
|
}
|
|
} else {
|
|
break :x_len &zero;
|
|
}
|
|
}
|
|
},
|
|
.length => |len| {
|
|
if (len.isZero()) {
|
|
break :x_len &zero;
|
|
}
|
|
},
|
|
.center => break :x_len &fifty,
|
|
}
|
|
break :x_len null;
|
|
};
|
|
|
|
const y_len: ?*const LengthPercentage = y_len: {
|
|
switch (this.y) {
|
|
.side => |side| {
|
|
if (side.side == .top) {
|
|
if (side.offset) |*offset| {
|
|
if (offset.isZero()) {
|
|
break :y_len &zero;
|
|
} else {
|
|
break :y_len offset;
|
|
}
|
|
} else {
|
|
break :y_len &zero;
|
|
}
|
|
}
|
|
},
|
|
.length => |len| {
|
|
if (len.isZero()) {
|
|
break :y_len &zero;
|
|
}
|
|
},
|
|
.center => break :y_len &fifty,
|
|
}
|
|
break :y_len null;
|
|
};
|
|
|
|
if (x_len != null and y_len != null) {
|
|
try x_len.?.toCss(W, dest);
|
|
try dest.writeStr(" ");
|
|
try y_len.?.toCss(W, dest);
|
|
} else {
|
|
try this.x.toCss(W, dest);
|
|
try dest.writeStr(" ");
|
|
try this.y.toCss(W, dest);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn default() @This() {
|
|
return .{
|
|
.x = HorizontalPosition{ .length = LengthPercentage{ .percentage = .{ .v = 0.0 } } },
|
|
.y = VerticalPosition{ .length = LengthPercentage{ .percentage = .{ .v = 0.0 } } },
|
|
};
|
|
}
|
|
|
|
/// Returns whether both the x and y positions are centered.
|
|
pub fn isCenter(this: *const @This()) bool {
|
|
return this.x.isCenter() and this.y.isCenter();
|
|
}
|
|
|
|
pub fn center() Position {
|
|
return .{ .x = .center, .y = .center };
|
|
}
|
|
|
|
pub fn eql(this: *const Position, other: *const Position) bool {
|
|
return this.x.eql(&other.x) and this.y.eql(&other.y);
|
|
}
|
|
|
|
pub fn isZero(this: *const Position) bool {
|
|
return this.x.isZero() and this.y.isZero();
|
|
}
|
|
|
|
pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() {
|
|
return css.implementDeepClone(@This(), this, allocator);
|
|
}
|
|
};
|
|
|
|
pub fn PositionComponent(comptime S: type) type {
|
|
return union(enum) {
|
|
/// The `center` keyword.
|
|
center,
|
|
/// A length or percentage from the top-left corner of the box.
|
|
length: LengthPercentage,
|
|
/// A side keyword with an optional offset.
|
|
side: struct {
|
|
/// A side keyword.
|
|
side: S,
|
|
/// Offset from the side.
|
|
offset: ?LengthPercentage,
|
|
|
|
pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() {
|
|
return css.implementDeepClone(@This(), this, allocator);
|
|
}
|
|
|
|
pub fn eql(this: *const @This(), other: *const @This()) bool {
|
|
return css.implementEql(@This(), this, other);
|
|
}
|
|
},
|
|
|
|
const This = @This();
|
|
|
|
pub fn isZero(this: *const This) bool {
|
|
if (this.* == .length and this.length.isZero()) return true;
|
|
return false;
|
|
}
|
|
|
|
pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() {
|
|
return css.implementDeepClone(@This(), this, allocator);
|
|
}
|
|
|
|
pub fn eql(this: *const This, other: *const This) bool {
|
|
return css.implementEql(@This(), this, other);
|
|
}
|
|
|
|
pub fn parse(input: *css.Parser) Result(This) {
|
|
if (input.tryParse(
|
|
struct {
|
|
fn parse(i: *css.Parser) Result(void) {
|
|
return i.expectIdentMatching("center");
|
|
}
|
|
}.parse,
|
|
.{},
|
|
).isOk()) {
|
|
return .{ .result = .center };
|
|
}
|
|
|
|
if (input.tryParse(LengthPercentage.parse, .{}).asValue()) |lp| {
|
|
return .{ .result = .{ .length = lp } };
|
|
}
|
|
|
|
const side = switch (S.parse(input)) {
|
|
.result => |vv| vv,
|
|
.err => |e| return .{ .err = e },
|
|
};
|
|
const offset = input.tryParse(LengthPercentage.parse, .{}).asValue();
|
|
return .{ .result = .{ .side = .{ .side = side, .offset = offset } } };
|
|
}
|
|
|
|
pub fn toCss(this: *const @This(), comptime W: type, dest: *css.Printer(W)) css.PrintErr!void {
|
|
switch (this.*) {
|
|
.center => {
|
|
if (dest.minify) {
|
|
try dest.writeStr("50%");
|
|
} else {
|
|
try dest.writeStr("center");
|
|
}
|
|
},
|
|
.length => |*lp| try lp.toCss(W, dest),
|
|
.side => |*s| {
|
|
try s.side.toCss(W, dest);
|
|
if (s.offset) |lp| {
|
|
try dest.writeStr(" ");
|
|
try lp.toCss(W, dest);
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn isCenter(this: *const This) bool {
|
|
switch (this.*) {
|
|
.center => return true,
|
|
.length => |*l| {
|
|
if (l.* == .percentage) return l.percentage.v == 0.5;
|
|
},
|
|
else => {},
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
}
|
|
|
|
pub const HorizontalPositionKeyword = enum {
|
|
/// The `left` keyword.
|
|
left,
|
|
/// The `right` keyword.
|
|
right,
|
|
|
|
pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) HorizontalPositionKeyword {
|
|
return css.implementDeepClone(@This(), this, allocator);
|
|
}
|
|
|
|
pub fn eql(this: *const HorizontalPositionKeyword, other: *const HorizontalPositionKeyword) bool {
|
|
return this.* == other.*;
|
|
}
|
|
|
|
pub fn asStr(this: *const @This()) []const u8 {
|
|
return css.enum_property_util.asStr(@This(), this);
|
|
}
|
|
|
|
pub fn parse(input: *css.Parser) Result(@This()) {
|
|
return css.enum_property_util.parse(@This(), input);
|
|
}
|
|
|
|
pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void {
|
|
return css.enum_property_util.toCss(@This(), this, W, dest);
|
|
}
|
|
|
|
pub fn intoLengthPercentage(this: *const @This()) LengthPercentage {
|
|
return switch (this.*) {
|
|
.left => LengthPercentage.zero(),
|
|
.right => .{ .percentage = .{ .v = 1.0 } },
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const VerticalPositionKeyword = enum {
|
|
/// The `top` keyword.
|
|
top,
|
|
/// The `bottom` keyword.
|
|
bottom,
|
|
|
|
pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() {
|
|
return css.implementDeepClone(@This(), this, allocator);
|
|
}
|
|
|
|
pub fn eql(this: *const VerticalPositionKeyword, other: *const VerticalPositionKeyword) bool {
|
|
return this.* == other.*;
|
|
}
|
|
|
|
pub fn asStr(this: *const @This()) []const u8 {
|
|
return css.enum_property_util.asStr(@This(), this);
|
|
}
|
|
|
|
pub fn parse(input: *css.Parser) Result(@This()) {
|
|
return css.enum_property_util.parse(@This(), input);
|
|
}
|
|
|
|
pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void {
|
|
return css.enum_property_util.toCss(@This(), this, W, dest);
|
|
}
|
|
|
|
pub fn intoLengthPercentage(this: *const @This()) LengthPercentage {
|
|
return switch (this.*) {
|
|
.top => LengthPercentage.zero(),
|
|
.bottom => LengthPercentage{ .percentage = Percentage{ .v = 1.0 } },
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const HorizontalPosition = PositionComponent(HorizontalPositionKeyword);
|
|
pub const VerticalPosition = PositionComponent(VerticalPositionKeyword);
|
|
|
|
const std = @import("std");
|