Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 38 additions & 61 deletions src/css/properties/font.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,14 +322,12 @@ pub enum FontFamily {
/// A custom family name.
// Arena-backed slice: the pointer targets bytes owned by the parser arena and is
// only valid while that arena is alive (becomes `&'bump [u8]` once the 'bump
// lifetime is threaded through these types). With `*const [u8]`, derived
// PartialEq/Eq/Hash would compare by pointer; the manual impls below
// hash/compare by content (Wyhash over bytes).
// lifetime is threaded through these types). With `*const [u8]`, a derived
// PartialEq would compare by pointer; the manual impl below compares by
// content.
Comment thread
robobun marked this conversation as resolved.
Resolved
FamilyName(*const [u8]),
}

type FontFamilyHashMap<V> = bun_collections::ArrayHashMap<FontFamily, V>;

impl FontFamily {
pub(crate) fn parse(input: &mut css::Parser) -> CssResult<Self> {
if let Ok(value) = input.try_parse(|p| p.expect_string().map(std::ptr::from_ref::<[u8]>)) {
Expand Down Expand Up @@ -417,12 +415,12 @@ impl FontFamily {
}
}

// eql / hash / deepClone — `PartialEq`/`Clone` hand-impls below; bridged to
// eql / deepClone — `PartialEq`/`Clone` hand-impls below; bridged to
// `CssEql`/`DeepClone` via `bridge_clone_partialeq!` in `generics.rs`.
}

// With `*const [u8]`, derived `PartialEq`/`Hash` would compare pointers;
// hand-roll to compare/hash the bytes by *content*.
// With `*const [u8]`, a derived `PartialEq` would compare pointers;
// hand-roll to compare the bytes by *content*.
Comment thread
robobun marked this conversation as resolved.
Resolved
impl PartialEq for FontFamily {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
Expand All @@ -435,23 +433,6 @@ impl PartialEq for FontFamily {
}
}
}
impl Eq for FontFamily {}

impl core::hash::Hash for FontFamily {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
// Hash the active tag then the payload bytes. With `*const [u8]` a
// derived Hash would hash the pointer address, breaking
// FontFamilyHashMap dedupe semantics.
core::mem::discriminant(self).hash(state);
match self {
FontFamily::Generic(g) => g.hash(state),
FontFamily::FamilyName(p) => {
// SAFETY: arena-owned slice valid for the parse session.
unsafe { (&**p).hash(state) }
}
}
}
}

impl Clone for FontFamily {
fn clone(&self) -> Self {
Expand Down Expand Up @@ -996,27 +977,6 @@ impl FontHandler {
let line_height: Option<LineHeight> = self.line_height.take();
let variant_caps: Option<FontVariantCaps> = self.variant_caps.take();

if let Some(f) = family.as_mut() {
if f.len() > 1 {
// Dedupe
let mut seen: FontFamilyHashMap<()> = Default::default();

let mut i: usize = 0;
while i < f.len() {
use bun_collections::array_hash_map::MapEntry;
match seen.entry(f.at(i).clone()) {
MapEntry::Occupied(_) => {
let _ = f.ordered_remove(i);
}
MapEntry::Vacant(v) => {
v.insert(());
i += 1;
}
}
}
}
}

if let (Some(_), Some(_), Some(_), Some(_), Some(_), Some(_), Some(variant_caps_v)) = (
family.as_ref(),
size.as_ref(),
Expand Down Expand Up @@ -1100,33 +1060,50 @@ const DEFAULT_SYSTEM_FONTS: &[&[u8]] = &[
b"Helvetica Neue",
];

fn system_font(name: &'static [u8]) -> FontFamily {
FontFamily::FamilyName(std::ptr::from_ref::<[u8]>(name))
}

#[inline]
fn compatible_font_family(
_family: Option<Vec<FontFamily>>,
family: Option<Vec<FontFamily>>,
is_supported: bool,
) -> Option<Vec<FontFamily>> {
let mut family = _family;
if is_supported {
return family;
}
let families = family?;
let Some(system_ui) = families.slice_const().iter().position(is_system_ui) else {
return Some(families);
};

// Unlike lightningcss, only a later copy of an inserted name is dropped. Dropping any other
// repeat can change rendering: https://github.com/parcel-bundler/lightningcss/issues/1221
Comment thread
robobun marked this conversation as resolved.
Resolved
let mut inserted = [true; DEFAULT_SYSTEM_FONTS.len()];
let mut expanded = Vec::with_capacity(families.len() + DEFAULT_SYSTEM_FONTS.len());
for (i, entry) in families.into_iter().enumerate() {
let fallback = DEFAULT_SYSTEM_FONTS
.iter()
.position(|&name| entry == system_font(name));
if let Some(fallback) = fallback {
if i < system_ui {
inserted[fallback] = false;
} else if inserted[fallback] {
continue;
}
}
expanded.push(entry);

if let Some(families) = family.as_mut() {
// Iterating the slice while inserting into `families` mid-loop would
// alias the &mut needed for `insert` (and `insert` may reallocate,
// invalidating the iterator).
// Capture the system-ui index first, drop the borrow, then
// perform the inserts using the captured index.
if let Some(i) = families.slice_const().iter().position(is_system_ui) {
for (j, name) in DEFAULT_SYSTEM_FONTS.iter().enumerate() {
families.insert(
i + j + 1,
FontFamily::FamilyName(std::ptr::from_ref::<[u8]>(*name)),
);
if i == system_ui {
for (fallback, &name) in DEFAULT_SYSTEM_FONTS.iter().enumerate() {
if inserted[fallback] {
expanded.push(system_font(name));
}
}
}
}

family
Some(expanded)
}

#[inline]
Expand Down
82 changes: 82 additions & 0 deletions test/js/bun/css/css.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5076,6 +5076,88 @@ describe("css tests", () => {
},
);

// A later copy of a name added after `system-ui` is dropped. Every other family stays as written.
for (const [source, expected] of [
[
"system-ui, Arial, Roboto",
"system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, Arial",
],
// `Roboto` comes before `system-ui`, so it is not added, and no copy of it is dropped.
[
"Roboto, system-ui, Roboto",
"Roboto, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, Roboto",
],
[
"Roboto, Roboto, system-ui",
"Roboto, Roboto, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Noto Sans, Ubuntu, Cantarell, Helvetica Neue",
],
[
"system-ui, system-ui",
"system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, system-ui",
],
[
"Foo, Foo, system-ui, monospace, monospace",
"Foo, Foo, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, monospace, monospace",
],
[
"system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, Arial",
"system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, Arial",
],
]) {
prefix_test(`.foo { font-family: ${source} }`, `.foo { font-family: ${expected}; }`, { firefox: 91 << 16 });
}

// The second rule merges into the first, and the merged declarations go through the font handler again.
prefix_test(
".foo { font-family: system-ui, Arial } .foo { color: red }",
".foo { color: red; font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, Arial; }",
{ firefox: 91 << 16 },
);

// Browsers give the lone generic `monospace` a smaller default size than `monospace, monospace`.
minify_test(
"pre{font-family:monospace,monospace;font-size:1em}",
"pre{font-family:monospace,monospace;font-size:1em}",
);
minify_test("code{font:1em monospace,monospace}", "code{font:1em monospace,monospace}");
minify_test(
".foo{font:10px serif;font-family:monospace,monospace;font-size:12px;font-style:italic;font-weight:bold;font-stretch:normal;font-variant-caps:small-caps;line-height:1.2em}",
".foo{font:italic small-caps 700 12px/1.2em monospace,monospace}",
);
minify_test("pre{font-family:monospace,monospace!important}", "pre{font-family:monospace,monospace!important}");
minify_test(
"@media print{pre{font-family:monospace,monospace}}",
"@media print{pre{font-family:monospace,monospace}}",
);
minify_test("kbd{font-family:monospace,monospace,monospace}", "kbd{font-family:monospace,monospace,monospace}");
minify_test("kbd{font-family:monospace,serif,monospace}", "kbd{font-family:monospace,serif,monospace}");
minify_test(".foo{font-family:Foo,Foo,serif}", ".foo{font-family:Foo,Foo,serif}");
minify_test(
"a{font-family:monospace,monospace}b{font-family:monospace}",
"a{font-family:monospace,monospace}b{font-family:monospace}",
);
minify_test("a{font-family:monospace,monospace}a{color:red}", "a{color:red;font-family:monospace,monospace}");

test("Bun.build keeps a repeated font family", async () => {
const build = async (minify: boolean) => {
const { outputs } = await Bun.build({
entrypoints: ["/normalize.css"],
files: {
"/normalize.css": "pre{font-family:monospace,monospace;font-size:1em}code{font:1em monospace,monospace}",
},
minify,
});
// The unminified output starts with a comment that names the entry point relative to the cwd.
return (await outputs[0].text()).replace(/^\/\*.*\*\/\n/, "");
};

expect({ minified: await build(true), unminified: await build(false) }).toEqual({
minified: "pre{font-family:monospace,monospace;font-size:1em}code{font:1em monospace,monospace}\n",
unminified:
"pre {\n font-family: monospace, monospace;\n font-size: 1em;\n}\n\ncode {\n font: 1em monospace, monospace;\n}\n",
});
});

prefix_test(
`
.foo {
Expand Down
Loading