From 927985f84731290a9324071c3e0964b4ebb5c651 Mon Sep 17 00:00:00 2001 From: red person Date: Wed, 28 Oct 2026 00:23:18 +0300 Subject: [PATCH] fix(fonts): group compound weight and style variants by family --- routes/font_routes.py | 5 +++-- tests/test_font_routes.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/routes/font_routes.py b/routes/font_routes.py index 3451db8c7..fbfd20458 100644 --- a/routes/font_routes.py +++ b/routes/font_routes.py @@ -19,9 +19,10 @@ def _split_family_token(token): def _derive_family(filename): """Derive a font-family name from a filename like 'JetBrainsMono-Regular.woff2' → 'JetBrains Mono'.""" name = os.path.splitext(filename)[0] - # Strip common weight/style suffixes + # Strip weight/style suffixes, including BoldItalic and Bold-Italic. name = re.sub( - r'[-_ ]?(Thin|ExtraLight|UltraLight|Light|Regular|Medium|SemiBold|DemiBold|Bold|ExtraBold|UltraBold|Black|Heavy|Italic|Oblique|Variable|VF)$', + r'[-_ ]?(Thin|ExtraLight|UltraLight|Light|Regular|Medium|SemiBold|DemiBold|Bold|ExtraBold|UltraBold|Black|Heavy|Italic|Oblique|Variable|VF)' + r'(?:[-_ ]?(?:Italic|Oblique))?$', '', name, flags=re.IGNORECASE ) # Replace dashes/underscores with spaces diff --git a/tests/test_font_routes.py b/tests/test_font_routes.py index e2a417ef8..a0e2b766e 100644 --- a/tests/test_font_routes.py +++ b/tests/test_font_routes.py @@ -1,3 +1,8 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +import routes.font_routes as font_routes from routes.font_routes import _derive_family @@ -9,3 +14,37 @@ def test_derive_family_splits_common_family_suffixes(): assert _derive_family("FiraCode-SemiBold.ttf") == "Fira Code" assert _derive_family("NotoSans-Bold.otf") == "Noto Sans" assert _derive_family("RobotoSlab-Bold.woff2") == "Roboto Slab" + + +@pytest.mark.parametrize("filename,expected", [ + ("JetBrainsMono-SemiBoldItalic.woff2", "JetBrains Mono"), + ("FiraCode-BoldItalic.ttf", "Fira Code"), + ("NotoSans-ExtraLightItalic.otf", "Noto Sans"), + ("RobotoSlab-UltraBoldOblique.WOFF2", "Roboto Slab"), + ("SourceSansPro-Bold-Italic.woff", "Source Sans Pro"), + ("FiraCode-SemiBold_Italic.ttf", "Fira Code"), + ("FiraCode-Italic.woff2", "Fira Code"), + ("FiraCode-VF.woff2", "Fira Code"), + ("MyFont-Book.woff2", "My Font Book"), +]) +def test_compound_weight_and_style_are_not_part_of_the_family(filename, expected): + assert _derive_family(filename) == expected + + +def test_custom_font_endpoint_groups_all_styles_under_one_family(tmp_path, monkeypatch): + files = ["JetBrainsMono-Regular.woff2", "JetBrainsMono-SemiBoldItalic.woff2", + "JetBrainsMono-Bold-Oblique.ttf"] + for name in [*files, "OtherFont-Regular.otf", "README.md"]: + (tmp_path / name).write_bytes(b"filename-only discovery fixture") + monkeypatch.setattr(font_routes, "CUSTOM_FONTS_DIR", str(tmp_path)) + app = FastAPI() + app.include_router(font_routes.setup_font_routes()) + with TestClient(app) as client: + response = client.get("/api/fonts/custom") + assert response.status_code == 200 + fonts = response.json()["fonts"] + assert set(fonts) == {"JetBrains Mono", "Other Font"} + assert fonts["JetBrains Mono"] == [ + {"file": name, "url": f"/static/fonts/custom/{name}", "format": name.rsplit(".", 1)[1]} + for name in sorted(files) + ]