First off, thank you for considering contributing to Fontspector! We're excited you're here. Every contribution, from a small typo fix to a new feature, is valuable.
This document provides guidelines to help you through the contribution process.
This project and everyone participating in it is governed by our Code of Conduct. By participating, you are expected to uphold this code. Please report unacceptable behavior.
Fontspector is a Rust workspace containing multiple crates. To get started:
- Fork the repository on GitHub.
- Clone your fork locally:
git clone https://github.com/YOUR-USERNAME/fontspector.git cd fontspector - Build the project to ensure everything is set up correctly:
cargo build --all
Fontspector is a font testing framework written in Rust. It has a number of core concepts:
- A check is a single test that can be run on a font. It is implemented as a
Checkstructure, which brings together the check implementation (a function) and metadata about the check. The#[check]attribute macro is used to define checks. We use the term "check" rather than "test" to avoid confusion with the unit tests; we check a font, and we test the checks. TheCheckstructure is defined infontspector-checkapi/src/check.rs. - Each check has a check ID. The mapping between check IDs and their implementation files can be found in
checks.txt. - A profile is a collection of checks that can be run together. Individual font manufacturers
define which checks they would like to run on their fonts by creating a profile.
Profiles are implemented as Rust crates that depend on the
fontspector-checkapicrate. The check implementations have a "home" within a specific profile, but can be reused in other profiles. TheProfilestructure is defined infontspector-checkapi/src/profile.rs. - A check returns a
CheckResult. Fontspector will run multiple checks on multiple fonts, and
each check may discover one or more problems with the font.CheckResultis a structure which wraps up everything that you need to report the result of a check: which file it was run on, which check was run, metadata to be displayed about the check (such as the reasoning behind the check and a user-friendly title), together with all of the results of the check (which may be zero or more problems found). TheCheckResultstructure is defined infontspector-checkapi/src/checkresult.rs. - A
Statusis a single reported problem found by a check. A check may return zero or moreStatusitems, each of which has a severity (error, warning, info, or pass), optionally a message (to be reported to the user) and a code (a short string that identifies the specific problem found, for example for unit tests or for advanced users who know what to expect). - The severity mentioned above is represented by the
StatusCodeenum. We should probably have called itSeverity, and maybe one day we will.StatusandStatusCodeare defined infontspector-checkapi/src/status.rs. - We don't simply check fonts. Fontspector can check HTML files, metadata files, and so on. But obviously only some checks apply to certain file types. Checks declare which
FileTypethey apply to. - Files are wrapped up in
Testablestructs, which include their contents and file name. To determine if the file can be converted into aFileTypeto be run by a particular check, we call.from_testableon theFileTypeenum. If it returnsSome, we can run the check; if it returnsNone, we skip the check for that file. TheFileTypeConverttrait also tells us what kind of representation the file can be converted into. For example,Testables which are TTF files can be converted intoTestFonts. TestFontinfontspector-checkapi/src/font.rscontains a number of helper methods which tests can use to manipulate the font and extract data from it.- Each check runs in a
Context. (fontspector-checkapi/src/context.rs) The context contains a general-purpose cache that checks can use to avoid recomputing things, user-defined per-check configuration, some free-form metadata, andOverrides which change the return values of a check.
We export the Fontspector check runner to a Python module, and then use
pytest to run (a modified version of) the fontbakery test suite. To
do this:
pip3 install -U maturin
cd fontspector-py
python3 -m venv venv ; . venv/bin/activate
pip install maturin
maturin develop
pytest
We welcome many types of contributions, including:
- New checks and features
- Bug fixes
- Documentation improvements
- Performance enhancements
-
In the previous incarnation of this QA tool, fontbakery, checks were largely granular; a single check would test for a single specific issue. This resulted in two problems: first, checks would be added without clearly checking whether they were duplicative of other checks; second, multiple checks on the same area of the font produced high overhead due to spinning up many checks and fetching the same data multiple times. In Fontspector, we have moved to a more holistic approach, where a single check may test for multiple related issues. This means that when adding a new check, you should first check whether there is an existing check that tests for the same area of the font in the same profile, and if so, consider adding your new test to that existing check rather than creating a new one. Ultimately this is a judgement call, as checks may have different rationales. But as a general rule, if your new test is related to an existing check and could be added without making the check unwieldy, it's better to add it to the existing check.
-
Checks should have Rust tests, and the tests should cover both passing and failing cases. If you're adding a new check, please add tests for it. Good examples of tests can be found in the
profile-universal/src/checks/arabic_high_hamza.rsandprofile-universal/src/checks/family_uniqueness_first_31_characters.rschecks. -
All warning and failing subresults should return rich
Metadatato help tools like Fontspector-Web to display the results in a user-friendly way. -
Where possible, tests should include a hotfix function that can be used to automatically fix the problem. This is not always possible, but where it is, it can be a great help to users. Where hotfixing is not possible, an explanation of how to fix the issue in font editors should be added to the rationale.
-
Create a branch for your changes:
git checkout -b feat/my-awesome-feature
-
Make your changes.
-
Ensure Code Quality: Before committing, please run the standard Rust formatting and linting tools across the entire workspace.
# Format your code cargo fmt --all # Run clippy to catch common mistakes and style issues cargo clippy --all -- -D warnings
-
Run Tests: Make sure all existing tests pass and, if you're adding a new feature, please add tests for it.
cargo test --all -
Commit Your Changes with Conventional Commits: We use Conventional Commits to automate changelogs and versioning. Your commit messages MUST follow this specification.
The commit message should be structured as follows:
<type>(<scope>): <short description> <BLANK LINE> <optional body> <BLANK LINE> <optional footer>Common types:
feat: A new feature.fix: A bug fix.docs: Documentation only changes.style: Changes that do not affect the meaning of the code (white-space, formatting, etc).refactor: A code change that neither fixes a bug nor adds a feature.perf: A code change that improves performance.test: Adding missing tests or correcting existing tests.chore: Changes to the build process or auxiliary tools.
Example:
feat(check-api): Add support for variable font axis checksUsing
cogfor commits: To simplify creating conventional commits, we strongly encourage usingcog(Cocogitto), which is configured for this project incog.toml. Instead ofgit commit, you can simply run:cog commit
This will guide you through creating a compliant commit message.
-
Push to your fork and open a Pull Request against the
mainbranch. -
PR Title: Because we squash and merge pull requests, the title of your PR must also be a valid Conventional Commit message. The title will become the commit message in the
mainbranch.
Please do not bump version numbers in Cargo.toml files or manually edit CHANGELOG.md files. This is handled automatically.
Our release process is automated using cargo-smart-release in a GitHub Action. When a release is triggered, the tool analyzes all Conventional Commit messages since the last tag. It then determines the correct semantic version bump (patch, minor, or major) for each affected crate and generates the corresponding changelog entries.
This is why your commit messages and PR titles are so important—they directly control the release process!
Thank you for contributing!