Files
bun.sh/src/css/properties/font.zig
2025-01-18 13:26:27 -08:00

1056 lines
36 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const std = @import("std");
const bun = @import("root").bun;
const Allocator = std.mem.Allocator;
pub const css = @import("../css_parser.zig");
const Error = css.Error;
const ArrayList = std.ArrayListUnmanaged;
const SmallList = css.SmallList;
const Printer = css.Printer;
const PrintErr = css.PrintErr;
const css_values = css.css_values;
const CssColor = css.css_values.color.CssColor;
const Image = css.css_values.image.Image;
const Length = css.css_values.length.LengthValue;
const LengthPercentage = css_values.length.LengthPercentage;
const LengthPercentageOrAuto = css_values.length.LengthPercentageOrAuto;
const PropertyCategory = css.PropertyCategory;
const LogicalGroup = css.LogicalGroup;
const CSSNumber = css.css_values.number.CSSNumber;
const CSSNumberFns = css.css_values.number.CSSNumberFns;
const CSSInteger = css.css_values.number.CSSInteger;
const NumberOrPercentage = css.css_values.percentage.NumberOrPercentage;
const Percentage = css.css_values.percentage.Percentage;
const Angle = css.css_values.angle.Angle;
const DashedIdentReference = css.css_values.ident.DashedIdentReference;
const Time = css.css_values.time.Time;
const EasingFunction = css.css_values.easing.EasingFunction;
const CustomIdent = css.css_values.ident.CustomIdent;
const CSSString = css.css_values.string.CSSString;
const DashedIdent = css.css_values.ident.DashedIdent;
const Url = css.css_values.url.Url;
const CustomIdentList = css.css_values.ident.CustomIdentList;
const Location = css.Location;
const HorizontalPosition = css.css_values.position.HorizontalPosition;
const VerticalPosition = css.css_values.position.VerticalPosition;
const ContainerName = css.css_rules.container.ContainerName;
/// A value for the [font-weight](https://www.w3.org/TR/css-fonts-4/#font-weight-prop) property.
pub const FontWeight = union(enum) {
/// An absolute font weight.
absolute: AbsoluteFontWeight,
/// The `bolder` keyword.
bolder,
/// The `lighter` keyword.
lighter,
// TODO: implement this
pub usingnamespace css.DeriveParse(@This());
pub usingnamespace css.DeriveToCss(@This());
pub inline fn default() FontWeight {
return .{ .absolute = AbsoluteFontWeight.default() };
}
pub fn isCompatible(this: *const FontWeight, browsers: bun.css.targets.Browsers) bool {
return switch (this.*) {
.absolute => |*a| a.isCompatible(browsers),
.bolder, .lighter => true,
};
}
pub fn eql(lhs: *const @This(), rhs: *const @This()) bool {
return css.implementEql(@This(), lhs, rhs);
}
pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() {
return css.implementDeepClone(@This(), this, allocator);
}
};
/// An [absolute font weight](https://www.w3.org/TR/css-fonts-4/#font-weight-absolute-values),
/// as used in the `font-weight` property.
///
/// See [FontWeight](FontWeight).
pub const AbsoluteFontWeight = union(enum) {
/// An explicit weight.
weight: CSSNumber,
/// Same as `400`.
normal,
/// Same as `700`.
bold,
pub usingnamespace css.DeriveParse(@This());
pub fn toCss(this: *const AbsoluteFontWeight, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void {
return switch (this.*) {
.weight => |*weight| CSSNumberFns.toCss(weight, W, dest),
.normal => try dest.writeStr(if (dest.minify) "400" else "normal"),
.bold => try dest.writeStr(if (dest.minify) "700" else "bold"),
};
}
pub fn isCompatible(this: *const AbsoluteFontWeight, browsers: bun.css.targets.Browsers) bool {
return switch (this.*) {
// Older browsers only supported 100, 200, 300, ...900 rather than arbitrary values.
.weight => |*val| if (!((val.* >= 100.0 and val.* <= 900.0) and @mod(val.*, 100.0) == 0.0))
css.Feature.font_weight_number.isCompatible(browsers)
else
true,
else => true,
};
}
pub inline fn default() AbsoluteFontWeight {
return .normal;
}
pub fn eql(lhs: *const AbsoluteFontWeight, rhs: *const AbsoluteFontWeight) bool {
return css.implementEql(@This(), lhs, rhs);
}
};
/// A value for the [font-size](https://www.w3.org/TR/css-fonts-4/#font-size-prop) property.
pub const FontSize = union(enum) {
/// An explicit size.
length: LengthPercentage,
/// An absolute font size keyword.
absolute: AbsoluteFontSize,
/// A relative font size keyword.
relative: RelativeFontSize,
pub usingnamespace css.DeriveParse(@This());
pub usingnamespace css.DeriveToCss(@This());
pub fn isCompatible(this: *const FontSize, browsers: bun.css.targets.Browsers) bool {
return switch (this.*) {
.length => |*l| switch (l.*) {
.dimension => |*d| switch (d.*) {
.rem => css.Feature.font_size_rem.isCompatible(browsers),
else => l.isCompatible(browsers),
},
else => l.isCompatible(browsers),
},
.absolute => |*a| a.isCompatible(browsers),
.relative => true,
};
}
pub fn eql(lhs: *const @This(), rhs: *const @This()) bool {
return css.implementEql(@This(), lhs, rhs);
}
pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() {
return css.implementDeepClone(@This(), this, allocator);
}
};
/// An [absolute font size](https://www.w3.org/TR/css-fonts-3/#absolute-size-value),
/// as used in the `font-size` property.
///
/// See [FontSize](FontSize).
pub const AbsoluteFontSize = enum {
/// "xx-small"
@"xx-small",
/// "x-small"
@"x-small",
/// "small"
small,
/// "medium"
medium,
/// "large"
large,
/// "x-large"
@"x-large",
/// "xx-large"
@"xx-large",
/// "xxx-large"
@"xxx-large",
pub usingnamespace css.DefineEnumProperty(@This());
pub fn isCompatible(this: *const AbsoluteFontSize, browsers: bun.css.targets.Browsers) bool {
return switch (this.*) {
.@"xxx-large" => css.Feature.font_size_x_x_x_large.isCompatible(browsers),
else => true,
};
}
};
/// A [relative font size](https://www.w3.org/TR/css-fonts-3/#relative-size-value),
/// as used in the `font-size` property.
///
/// See [FontSize](FontSize).
pub const RelativeFontSize = enum {
smaller,
larger,
pub usingnamespace css.DefineEnumProperty(@This());
};
/// A value for the [font-stretch](https://www.w3.org/TR/css-fonts-4/#font-stretch-prop) property.
pub const FontStretch = union(enum) {
/// A font stretch keyword.
keyword: FontStretchKeyword,
/// A percentage.
percentage: Percentage,
// TODO: implement this
pub usingnamespace css.DeriveParse(@This());
pub fn toCss(this: *const FontStretch, comptime W: type, dest: *css.Printer(W)) css.PrintErr!void {
if (dest.minify) {
const percentage: Percentage = this.intoPercentage();
return percentage.toCss(W, dest);
}
return switch (this.*) {
.percentage => |*val| val.toCss(W, dest),
.keyword => |*kw| kw.toCss(W, dest),
};
}
pub fn intoPercentage(this: *const FontStretch) Percentage {
return switch (this.*) {
.percentage => |*val| val.*,
.keyword => |*kw| kw.intoPercentage(),
};
}
pub fn isCompatible(this: *const FontStretch, browsers: bun.css.targets.Browsers) bool {
return switch (this.*) {
.percentage => css.Feature.font_stretch_percentage.isCompatible(browsers),
.keyword => true,
};
}
pub fn eql(lhs: *const FontStretch, rhs: *const FontStretch) bool {
return css.implementEql(@This(), lhs, rhs);
}
pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() {
return css.implementDeepClone(@This(), this, allocator);
}
pub inline fn default() FontStretch {
return .{ .keyword = FontStretchKeyword.default() };
}
};
/// A [font stretch keyword](https://www.w3.org/TR/css-fonts-4/#font-stretch-prop),
/// as used in the `font-stretch` property.
///
/// See [FontStretch](FontStretch).
pub const FontStretchKeyword = enum {
/// 100%
normal,
/// 50%
@"ultra-condensed",
/// 62.5%
@"extra-condensed",
/// 75%
condensed,
/// 87.5%
@"semi-condensed",
/// 112.5%
@"semi-expanded",
/// 125%
expanded,
/// 150%
@"extra-expanded",
/// 200%
@"ultra-expanded",
pub usingnamespace css.DefineEnumProperty(@This());
pub inline fn default() FontStretchKeyword {
return .normal;
}
pub fn intoPercentage(this: *const FontStretchKeyword) Percentage {
const val: f32 = switch (this.*) {
.@"ultra-condensed" => 0.5,
.@"extra-condensed" => 0.625,
.condensed => 0.75,
.@"semi-condensed" => 0.875,
.normal => 1.0,
.@"semi-expanded" => 1.125,
.expanded => 1.25,
.@"extra-expanded" => 1.5,
.@"ultra-expanded" => 2.0,
};
return .{ .v = val };
}
};
/// A value for the [font-family](https://www.w3.org/TR/css-fonts-4/#font-family-prop) property.
pub const FontFamily = union(enum) {
/// A generic family name.
generic: GenericFontFamily,
/// A custom family name.
family_name: []const u8,
pub fn HashMap(comptime V: type) type {
return std.ArrayHashMapUnmanaged(FontFamily, V, struct {
pub fn hash(_: @This(), key: FontFamily) u32 {
var hasher = std.hash.Wyhash.init(0);
key.hash(&hasher);
return @truncate(hasher.final());
}
pub fn eql(_: @This(), a: FontFamily, b: FontFamily, _: usize) bool {
return a.eql(&b);
}
}, false);
}
pub fn parse(input: *css.Parser) css.Result(@This()) {
if (input.tryParse(css.Parser.expectString, .{}).asValue()) |value| {
return .{ .result = .{ .family_name = value } };
}
if (input.tryParse(GenericFontFamily.parse, .{}).asValue()) |value| {
return .{ .result = .{ .generic = value } };
}
const stralloc = input.allocator();
const value = switch (input.expectIdent()) {
.result => |v| v,
.err => |e| return .{ .err = e },
};
var string: ?ArrayList(u8) = null;
while (input.tryParse(css.Parser.expectIdent, .{}).asValue()) |ident| {
if (string == null) {
string = ArrayList(u8){};
string.?.appendSlice(stralloc, value) catch bun.outOfMemory();
}
if (string) |*s| {
s.append(stralloc, ' ') catch bun.outOfMemory();
s.appendSlice(stralloc, ident) catch bun.outOfMemory();
}
}
const final_value = if (string) |s| s.items else value;
return .{ .result = .{ .family_name = final_value } };
}
pub fn toCss(this: *const @This(), comptime W: type, dest: *Printer(W)) PrintErr!void {
switch (this.*) {
.generic => |val| {
try val.toCss(W, dest);
},
.family_name => |val| {
// Generic family names such as sans-serif must be quoted if parsed as a string.
// CSS wide keywords, as well as "default", must also be quoted.
// https://www.w3.org/TR/css-fonts-4/#family-name-syntax
if (val.len > 0 and
!css.parse_utility.parseString(
dest.allocator,
GenericFontFamily,
val,
GenericFontFamily.parse,
).isOk()) {
var id = ArrayList(u8){};
defer id.deinit(dest.allocator);
var first = true;
var split_iter = std.mem.splitScalar(u8, val, ' ');
while (split_iter.next()) |slice| {
if (first) {
first = false;
} else {
id.append(dest.allocator, ' ') catch bun.outOfMemory();
}
const dest_id = id.writer(dest.allocator);
css.serializer.serializeIdentifier(slice, dest_id) catch return dest.addFmtError();
}
if (id.items.len < val.len + 2) {
return dest.writeStr(id.items);
}
}
return css.serializer.serializeString(val, dest) catch return dest.addFmtError();
},
}
}
pub fn isCompatible(this: *const FontFamily, browsers: bun.css.targets.Browsers) bool {
return switch (this.*) {
.generic => |g| g.isCompatible(browsers),
.family_name => true,
};
}
pub fn eql(lhs: *const @This(), rhs: *const @This()) bool {
return css.implementEql(@This(), lhs, rhs);
}
pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() {
return css.implementDeepClone(@This(), this, allocator);
}
pub fn hash(this: *const @This(), hasher: anytype) void {
return css.implementHash(@This(), this, hasher);
}
};
/// A [generic font family](https://www.w3.org/TR/css-fonts-4/#generic-font-families) name,
/// as used in the `font-family` property.
///
/// See [FontFamily](FontFamily).
pub const GenericFontFamily = enum {
serif,
@"sans-serif",
cursive,
fantasy,
monospace,
@"system-ui",
emoji,
math,
fangsong,
@"ui-serif",
@"ui-sans-serif",
@"ui-monospace",
@"ui-rounded",
// CSS wide keywords. These must be parsed as identifiers so they
// don't get serialized as strings.
// https://www.w3.org/TR/css-values-4/#common-keywords
initial,
inherit,
unset,
// Default is also reserved by the <custom-ident> type.
// https://www.w3.org/TR/css-values-4/#custom-idents
default,
// CSS defaulting keywords
// https://drafts.csswg.org/css-cascade-5/#defaulting-keywords
revert,
@"revert-layer",
pub usingnamespace css.DefineEnumProperty(@This());
pub fn isCompatible(this: *const GenericFontFamily, browsers: bun.css.targets.Browsers) bool {
return switch (this.*) {
.@"system-ui" => css.Feature.font_family_system_ui.isCompatible(browsers),
.@"ui-serif", .@"ui-sans-serif", .@"ui-monospace", .@"ui-rounded" => css.Feature.extended_system_fonts.isCompatible(browsers),
else => true,
};
}
};
/// A value for the [font-style](https://www.w3.org/TR/css-fonts-4/#font-style-prop) property.
pub const FontStyle = union(enum) {
/// Normal font style.
normal,
/// Italic font style.
italic,
/// Oblique font style, with a custom angle.
oblique: Angle,
pub fn default() FontStyle {
return .normal;
}
pub fn parse(input: *css.Parser) css.Result(FontStyle) {
const location = input.currentSourceLocation();
const ident = switch (input.expectIdent()) {
.result => |v| v,
.err => |e| return .{ .err = e },
};
// todo_stuff.match_ignore_ascii_case
if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("normal", ident)) {
return .{ .result = .normal };
} else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("italic", ident)) {
return .{ .result = .italic };
} else if (bun.strings.eqlCaseInsensitiveASCIIICheckLength("oblique", ident)) {
const angle = input.tryParse(Angle.parse, .{}).unwrapOr(FontStyle.defaultObliqueAngle());
return .{ .result = .{ .oblique = angle } };
} else {
//
return .{ .err = location.newUnexpectedTokenError(.{ .ident = ident }) };
}
}
pub fn toCss(this: *const FontStyle, comptime W: type, dest: *Printer(W)) PrintErr!void {
switch (this.*) {
.normal => try dest.writeStr("normal"),
.italic => try dest.writeStr("italic"),
.oblique => |angle| {
try dest.writeStr("oblique");
if (!angle.eql(&FontStyle.defaultObliqueAngle())) {
try dest.writeChar(' ');
try angle.toCss(W, dest);
}
},
}
}
pub fn isCompatible(this: *const FontStyle, browsers: bun.css.targets.Browsers) bool {
return switch (this.*) {
.oblique => |*angle| if (!angle.eql(&FontStyle.defaultObliqueAngle()))
css.Feature.font_style_oblique_angle.isCompatible(browsers)
else
true,
.normal, .italic => true,
};
}
pub fn defaultObliqueAngle() Angle {
return Angle{ .deg = 14.0 };
}
pub fn eql(lhs: *const @This(), rhs: *const @This()) bool {
return css.implementEql(@This(), lhs, rhs);
}
pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() {
return css.implementDeepClone(@This(), this, allocator);
}
};
/// A value for the [font-variant-caps](https://www.w3.org/TR/css-fonts-4/#font-variant-caps-prop) property.
pub const FontVariantCaps = enum {
/// No special capitalization features are applied.
normal,
/// The small capitals feature is used for lower case letters.
@"small-caps",
/// Small capitals are used for both upper and lower case letters.
@"all-small-caps",
/// Petite capitals are used.
@"petite-caps",
/// Petite capitals are used for both upper and lower case letters.
@"all-petite-caps",
/// Enables display of mixture of small capitals for uppercase letters with normal lowercase letters.
unicase,
/// Uses titling capitals.
@"titling-caps",
pub usingnamespace css.DefineEnumProperty(@This());
pub fn default() FontVariantCaps {
return .normal;
}
fn isCss2(this: *const FontVariantCaps) bool {
return switch (this.*) {
.normal, .@"small-caps" => true,
else => false,
};
}
pub fn parseCss2(input: *css.Parser) css.Result(FontVariantCaps) {
const value = switch (FontVariantCaps.parse(input)) {
.result => |v| v,
.err => |e| return .{ .err = e },
};
if (!value.isCss2()) {
return .{ .err = input.newCustomError(css.ParserError.invalid_value) };
}
return .{ .result = value };
}
pub fn isCompatible(_: *const FontVariantCaps, _: bun.css.targets.Browsers) bool {
return true;
}
};
/// A value for the [line-height](https://www.w3.org/TR/2020/WD-css-inline-3-20200827/#propdef-line-height) property.
pub const LineHeight = union(enum) {
/// The UA sets the line height based on the font.
normal,
/// A multiple of the element's font size.
number: CSSNumber,
/// An explicit height.
length: LengthPercentage,
pub usingnamespace @call(.auto, css.DeriveParse, .{@This()});
pub usingnamespace @call(.auto, css.DeriveToCss, .{@This()});
pub fn isCompatible(this: *const LineHeight, browsers: bun.css.targets.Browsers) bool {
return switch (this.*) {
.length => |*l| l.isCompatible(browsers),
.normal, .number => true,
};
}
pub fn eql(lhs: *const @This(), rhs: *const @This()) bool {
return css.implementEql(@This(), lhs, rhs);
}
pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() {
return css.implementDeepClone(@This(), this, allocator);
}
pub fn default() LineHeight {
return .normal;
}
};
/// A value for the [font](https://www.w3.org/TR/css-fonts-4/#font-prop) shorthand property.
pub const Font = struct {
/// The font family.
family: bun.BabyList(FontFamily),
/// The font size.
size: FontSize,
/// The font style.
style: FontStyle,
/// The font weight.
weight: FontWeight,
/// The font stretch.
stretch: FontStretch,
/// The line height.
line_height: LineHeight,
/// How the text should be capitalized. Only CSS 2.1 values are supported.
variant_caps: FontVariantCaps,
pub usingnamespace css.DefineShorthand(@This(), css.PropertyIdTag.font);
pub const PropertyFieldMap = .{
.family = css.PropertyIdTag.@"font-family",
.size = css.PropertyIdTag.@"font-size",
.style = css.PropertyIdTag.@"font-style",
.weight = css.PropertyIdTag.@"font-weight",
.stretch = css.PropertyIdTag.@"font-stretch",
.line_height = css.PropertyIdTag.@"line-height",
.variant_caps = css.PropertyIdTag.@"font-variant-caps",
};
pub fn parse(input: *css.Parser) css.Result(Font) {
var style: ?FontStyle = null;
var weight: ?FontWeight = null;
var stretch: ?FontStretch = null;
var size: ?FontSize = null;
var variant_caps: ?FontVariantCaps = null;
var count: i32 = 0;
while (true) {
// Skip "normal" since it is valid for several properties, but we don't know which ones it will be used for yet.
if (input.tryParse(css.Parser.expectIdentMatching, .{"normal"}).isOk()) {
count += 1;
continue;
}
if (style == null) {
if (input.tryParse(FontStyle.parse, .{}).asValue()) |value| {
style = value;
count += 1;
continue;
}
}
if (weight == null) {
if (input.tryParse(FontWeight.parse, .{}).asValue()) |value| {
weight = value;
count += 1;
continue;
}
}
if (variant_caps != null) {
if (input.tryParse(FontVariantCaps.parseCss2, .{}).asValue()) |value| {
variant_caps = value;
count += 1;
continue;
}
}
if (stretch == null) {
if (input.tryParse(FontStretchKeyword.parse, .{}).asValue()) |value| {
stretch = .{ .keyword = value };
count += 1;
continue;
}
}
size = switch (@call(.auto, @field(FontSize, "parse"), .{input})) {
.result => |v| v,
.err => |e| return .{ .err = e },
};
break;
}
if (count > 4) return .{ .err = input.newCustomError(css.ParserError.invalid_declaration) };
const final_size = size orelse return .{ .err = input.newCustomError(css.ParserError.invalid_declaration) };
const line_height = if (input.tryParse(css.Parser.expectDelim, .{'/'}).isOk()) switch (LineHeight.parse(input)) {
.result => |v| v,
.err => |e| return .{ .err = e },
} else null;
const family = switch (bun.BabyList(FontFamily).parse(input)) {
.result => |v| v,
.err => |e| return .{ .err = e },
};
return .{ .result = Font{
.family = family,
.size = final_size,
.style = style orelse FontStyle.default(),
.weight = weight orelse FontWeight.default(),
.stretch = stretch orelse FontStretch.default(),
.line_height = line_height orelse LineHeight.default(),
.variant_caps = variant_caps orelse FontVariantCaps.default(),
} };
}
pub fn toCss(this: *const Font, comptime W: type, dest: *Printer(W)) PrintErr!void {
if (!this.style.eql(&FontStyle.default())) {
try this.style.toCss(W, dest);
try dest.writeChar(' ');
}
if (!this.variant_caps.eql(&FontVariantCaps.default())) {
try this.variant_caps.toCss(W, dest);
try dest.writeChar(' ');
}
if (!this.weight.eql(&FontWeight.default())) {
try this.weight.toCss(W, dest);
try dest.writeChar(' ');
}
if (!this.stretch.eql(&FontStretch.default())) {
try this.stretch.toCss(W, dest);
try dest.writeChar(' ');
}
try this.size.toCss(W, dest);
if (!this.line_height.eql(&LineHeight.default())) {
try dest.delim('/', true);
try this.line_height.toCss(W, dest);
}
try dest.writeChar(' ');
const len = this.family.len;
for (this.family.sliceConst(), 0..) |*val, idx| {
try val.toCss(W, dest);
if (idx < len - 1) {
try dest.delim(',', false);
}
}
}
pub fn eql(lhs: *const @This(), rhs: *const @This()) bool {
return css.implementEql(@This(), lhs, rhs);
}
pub fn deepClone(this: *const @This(), allocator: std.mem.Allocator) @This() {
return css.implementDeepClone(@This(), this, allocator);
}
};
/// A value for the [vertical align](https://drafts.csswg.org/css2/#propdef-vertical-align) property.
// TODO: there is a more extensive spec in CSS3 but it doesn't seem any browser implements it? https://www.w3.org/TR/css-inline-3/#transverse-alignment
pub const VerticalAlign = union(enum) {
/// A vertical align keyword.
keyword: VerticalAlignKeyword,
/// An explicit length.
length: LengthPercentage,
};
/// A keyword for the [vertical align](https://drafts.csswg.org/css2/#propdef-vertical-align) property.
pub const VerticalAlignKeyword = enum {
/// Align the baseline of the box with the baseline of the parent box.
baseline,
/// Lower the baseline of the box to the proper position for subscripts of the parents box.
sub,
/// Raise the baseline of the box to the proper position for superscripts of the parents box.
super,
/// Align the top of the aligned subtree with the top of the line box.
top,
/// Align the top of the box with the top of the parents content area.
@"text-top",
/// Align the vertical midpoint of the box with the baseline of the parent box plus half the x-height of the parent.
middle,
/// Align the bottom of the aligned subtree with the bottom of the line box.
bottom,
/// Align the bottom of the box with the bottom of the parents content area.
@"text-bottom",
pub usingnamespace css.DefineEnumProperty(@This());
};
pub const FontProperty = packed struct(u8) {
@"font-family": bool = false,
@"font-size": bool = false,
@"font-style": bool = false,
@"font-weight": bool = false,
@"font-stretch": bool = false,
@"line-height": bool = false,
@"font-variant-caps": bool = false,
__unused: u1 = 0,
pub usingnamespace css.Bitflags(@This());
const FONT = FontProperty{
.@"font-family" = true,
.@"font-size" = true,
.@"font-style" = true,
.@"font-weight" = true,
.@"font-stretch" = true,
.@"line-height" = true,
.@"font-variant-caps" = true,
};
pub fn tryFromPropertyId(property_id: css.PropertyIdTag) ?FontProperty {
inline for (std.meta.fields(FontProperty)) |field| {
if (comptime std.mem.eql(u8, field.name, "__unused")) continue;
const desired = comptime @field(css.PropertyIdTag, field.name);
if (desired == property_id) {
var result: FontProperty = .{};
@field(result, field.name) = true;
return result;
}
}
if (property_id == .font) {
return FontProperty.FONT;
}
return null;
}
};
pub const FontHandler = struct {
family: ?bun.BabyList(FontFamily) = null,
size: ?FontSize = null,
style: ?FontStyle = null,
weight: ?FontWeight = null,
stretch: ?FontStretch = null,
line_height: ?LineHeight = null,
variant_caps: ?FontVariantCaps = null,
flushed_properties: FontProperty = .{},
has_any: bool = false,
pub fn handleProperty(
this: *FontHandler,
property: *const css.Property,
dest: *css.DeclarationList,
context: *css.PropertyHandlerContext,
) bool {
switch (property.*) {
.@"font-family" => |*val| this.propertyHelper(dest, context, "family", val),
.@"font-size" => |*val| this.propertyHelper(dest, context, "size", val),
.@"font-style" => |*val| this.propertyHelper(dest, context, "style", val),
.@"font-weight" => |*val| this.propertyHelper(dest, context, "weight", val),
.@"font-stretch" => |*val| this.propertyHelper(dest, context, "stretch", val),
.@"font-variant-caps" => |*val| this.propertyHelper(dest, context, "variant_caps", val),
.@"line-height" => |*val| this.propertyHelper(dest, context, "line_height", val),
.font => |*val| {
this.flushHelper(dest, context, "family", &val.family);
this.flushHelper(dest, context, "size", &val.size);
this.flushHelper(dest, context, "style", &val.style);
this.flushHelper(dest, context, "weight", &val.weight);
this.flushHelper(dest, context, "stretch", &val.stretch);
this.flushHelper(dest, context, "line_height", &val.line_height);
this.flushHelper(dest, context, "variant_caps", &val.variant_caps);
this.family = css.generic.deepClone(bun.BabyList(FontFamily), &val.family, context.allocator);
this.size = val.size.deepClone(context.allocator);
this.style = val.style.deepClone(context.allocator);
this.weight = val.weight.deepClone(context.allocator);
this.stretch = val.stretch.deepClone(context.allocator);
this.line_height = val.line_height.deepClone(context.allocator);
this.variant_caps = val.variant_caps.deepClone(context.allocator);
this.has_any = true;
// TODO: reset other properties
},
.unparsed => |*val| {
if (isFontProperty(val.property_id)) {
this.flush(dest, context);
this.flushed_properties.insert(FontProperty.tryFromPropertyId(val.property_id).?);
dest.append(context.allocator, property.*) catch bun.outOfMemory();
} else {
return false;
}
},
else => return false,
}
return true;
}
inline fn propertyHelper(this: *FontHandler, dest: *css.DeclarationList, context: *css.PropertyHandlerContext, comptime prop: []const u8, val: anytype) void {
this.flushHelper(dest, context, prop, val);
@field(this, prop) = css.generic.deepClone(@TypeOf(val.*), val, context.allocator);
this.has_any = true;
}
inline fn flushHelper(
this: *FontHandler,
dest: *css.DeclarationList,
context: *css.PropertyHandlerContext,
comptime prop: []const u8,
val: anytype,
) void {
if (@field(this, prop) != null and
!css.generic.eql(@TypeOf(@field(this, prop).?), &@field(this, prop).?, val) and
context.targets.browsers != null and
!css.generic.isCompatible(@TypeOf(@field(this, prop).?), val, context.targets.browsers.?))
{
this.flush(dest, context);
}
}
pub fn finalize(this: *FontHandler, decls: *css.DeclarationList, context: *css.PropertyHandlerContext) void {
this.flush(decls, context);
this.flushed_properties = .{};
}
fn flush(this: *FontHandler, decls: *css.DeclarationList, context: *css.PropertyHandlerContext) void {
const push = struct {
fn push(self: *FontHandler, d: *css.DeclarationList, ctx: *css.PropertyHandlerContext, comptime prop: []const u8, val: anytype) void {
d.append(ctx.allocator, @unionInit(css.Property, prop, val)) catch bun.outOfMemory();
var insertion: FontProperty = .{};
if (comptime std.mem.eql(u8, prop, "font")) {
insertion = FontProperty.FONT;
} else {
@field(insertion, prop) = true;
}
self.flushed_properties.insert(insertion);
}
}.push;
if (!this.has_any) {
return;
}
this.has_any = false;
var family: ?bun.BabyList(FontFamily) = bun.take(&this.family);
if (!this.flushed_properties.contains(FontProperty{ .@"font-family" = true })) {
family = compatibleFontFamily(context.allocator, family, !context.targets.shouldCompileSame(.font_family_system_ui));
}
const size: ?FontSize = bun.take(&this.size);
const style: ?FontStyle = bun.take(&this.style);
const weight: ?FontWeight = bun.take(&this.weight);
const stretch: ?FontStretch = bun.take(&this.stretch);
const line_height: ?LineHeight = bun.take(&this.line_height);
const variant_caps: ?FontVariantCaps = bun.take(&this.variant_caps);
if (family) |*f| {
if (f.len > 1) {
// Dedupe
var sfb = std.heap.stackFallback(664, bun.default_allocator);
const alloc = sfb.get();
var seen = FontFamily.HashMap(void){};
defer seen.deinit(alloc);
var i: usize = 0;
while (i < f.len) {
const gop = seen.getOrPut(alloc, f.at(i).*) catch bun.outOfMemory();
if (gop.found_existing) {
_ = f.orderedRemove(i);
} else {
i += 1;
}
}
}
}
if (family != null and size != null and style != null and weight != null and stretch != null and line_height != null and variant_caps != null) {
const caps = variant_caps.?;
push(this, decls, context, "font", Font{
.family = family.?,
.size = size.?,
.style = style.?,
.weight = weight.?,
.stretch = stretch.?,
.line_height = line_height.?,
.variant_caps = if (caps.isCss2()) caps else FontVariantCaps.default(),
});
// The `font` property only accepts CSS 2.1 values for font-variant caps.
// If we have a CSS 3+ value, we need to add a separate property.
if (!caps.isCss2()) {
push(this, decls, context, "font-variant-caps", caps);
}
} else {
if (family) |val| {
push(this, decls, context, "font-family", val);
}
if (size) |val| {
push(this, decls, context, "font-size", val);
}
if (style) |val| {
push(this, decls, context, "font-style", val);
}
if (variant_caps) |val| {
push(this, decls, context, "font-variant-caps", val);
}
if (weight) |val| {
push(this, decls, context, "font-weight", val);
}
if (stretch) |val| {
push(this, decls, context, "font-stretch", val);
}
if (line_height) |val| {
push(this, decls, context, "line-height", val);
}
}
}
};
const SYSTEM_UI: FontFamily = FontFamily{ .generic = .@"system-ui" };
const DEFAULT_SYSTEM_FONTS: []const []const u8 = &.{
// #1: Supported as the '-apple-system' value (macOS, Safari >= 9.2 < 11, Firefox >= 43)
"-apple-system",
// #2: Supported as the 'BlinkMacSystemFont' value (macOS, Chrome < 56)
"BlinkMacSystemFont",
"Segoe UI", // Windows >= Vista
"Roboto", // Android >= 4
"Noto Sans", // Plasma >= 5.5
"Ubuntu", // Ubuntu >= 10.10
"Cantarell", // GNOME >= 3
"Helvetica Neue",
};
inline fn compatibleFontFamily(allocator: std.mem.Allocator, _family: ?bun.BabyList(FontFamily), is_supported: bool) ?bun.BabyList(FontFamily) {
var family = _family;
if (is_supported) {
return family;
}
if (family) |*families| {
for (families.sliceConst(), 0..) |v, i| {
if (v.eql(&SYSTEM_UI)) {
for (DEFAULT_SYSTEM_FONTS, 0..) |name, j| {
families.insert(allocator, i + j + 1, .{ .family_name = name }) catch bun.outOfMemory();
}
break;
}
}
}
return family;
}
inline fn isFontProperty(property_id: css.PropertyId) bool {
return switch (property_id) {
.@"font-family",
.@"font-size",
.@"font-style",
.@"font-weight",
.@"font-stretch",
.@"font-variant-caps",
.@"line-height",
.font,
=> true,
else => false,
};
}