# Notes on removing ligatures from Fira Code
... to make a drop-in replacement of (now [unmaintained](https://github.com/bBoxType/FiraSans/issues/4#issuecomment-699882058)) Fira Mono.
> [!Important] "Show me the code"
> The development happens in: <https://github.com/qbane/FiraCodeNL>.
> The *build* is considered stable, but the modifications to Glyphs source code are ad-hoc, not easily mergable with upstream, and should be considered half-done.
## Background
The repo of Fira Mono: https://github.com/mozilla/Fira. The last version is 3.206.
The repo of Fira Code: https://github.com/tonsky/FiraCode. As of writing (2022/6), the latest version number is 6.2, published in late 2021. There is a milestone to v7, but no breaking change has been published.
By the way, Fira Sans had been maintained for a while as FiraGO, whose last commit was in 2018, [but no plan was for Fira Mono](https://github.com/bBoxType/FiraGO/issues/16). So sad.
There is an official [branch](https://github.com/tonsky/FiraCode/tree/fixed) that has built a variant that removes ligatures called "Fira Code Fixed", but it is only available for v5.2, with no changelog to follow. We use this as a blueprint; see [this section](#Diff-of-Fira-Code-52-vs-fixed) for details.
# File format
After some reading into the source, all the ingredient to the font is contained in the glyph file `FiraCode.glyphs`. It is possibly not the only file we need to work on (more on that later). First, it is in an obscure format called ASCII (old-style) plist format. We had better convert it to JSON to take advantage of existing JSON diffing tools (by the way, my favorite web-based diffing tool is https://jsoneditoronline.org/):
```python
import sys
import json
# pip install openstep_plist
import openstep_plist
with open(sys.argv[1], 'r') as fp:
data = openstep_plist.load(fp, use_numbers=True)
text = json.dumps(data, sort_keys=True, indent=2)
print(text)
```
Second, the semantics is seemingly proprietary, and the repo even contains a simple decoder/encoder written in Clojure for it to make variants. There is a libary [googlefonts/glyphsLib](https://github.com/googlefonts/glyphsLib) to help us on exploring the format.
Since I am not familar with font mechinary, I am making this document alongside the repository toward the goal.
Now it's time for the cherry picking...
# Patching Fira Code
Let us summarize and categorize each and every feature, determining the set of features to be kept. Here is my policy (very subjective):
* The default config, even with `liga` and `calt` both enabled, should be ligature-free.
* The appearence of a char should not be combined into its surroundings. This is to reduce the possibility to mistake some character combinations with an arbitrary single Unicode glyph.
* Punctuation tuning is allowed as-is.
* How about alphabetics such as matching case? 0"x"FF?
> [!WARNING] **Update 2024/7/17**
> This is fine if you use the font as a display font, but need more considerations as an editor font, where vertical space tuning can have [distracting effects](https://github.com/zed-industries/zed/issues/13060) for some people.
* Preserve character variations from Fira Code for aesthetic purposes; should do no harm as they are off by default.
Below is a quick reference to each ssXX/cvXX ([wiki](https://github.com/tonsky/FiraCode/wiki/How-to-enable-stylistic-sets)):
| Code | Character(s) | :accept:? | Notes |
| -------- | ------------- | --------- | ----- |
| ss01 | `r` | :heavy_check_mark:
| ss02 | `<=` → ≤; `>=` → ≥ | :x:
| ss03 | `&` | :heavy_check_mark:
| ss04 | `$` | :heavy_check_mark:
| ss05 | `@` | :heavy_check_mark:
| ss06 | Thin `\` when escaping | :heavy_check_mark:
| ss07 | `=~` → ≈; `!~` → ≉ | :x:
| ss08 | Small gaps in `==`-s | :x: | become void automatically <sup>*1</sup>
| ss09 | <div class="_box">`<<=` `>>=` <br> `\|=` `\|\|=`</div> to arrow | :x:
| ss10 | Connected Tl fl fi ... | :heavy_check_mark: :thinking_face: | keep for bw comp. w/ Fira Mono <sup>*2</sup>
| cv01 | `a` | :heavy_check_mark:
| cv02 | `g` | :heavy_check_mark:
| cv03..06 | `i` | :heavy_check_mark:
| cv07..10 | `l` | :heavy_check_mark:
| cv11..13 | `0` (zero) | :heavy_check_mark: | see also `zero`
| cv14 | `3` | :heavy_check_mark:
| cv15..16 | `*` | :heavy_check_mark:
| cv17 | `~` | :heavy_check_mark:
| cv18 | `%` | :heavy_check_mark: | <sup>*3</sup>
| cv19..20 | `<=` → (19: ≤; 20: ⇐) | :x:
| cv21..22 | `=<` → (21: ⩽; 22: ≤) | :x:
| cv23 | `>=` → ≥ | :x:
| cv24 | `/=` → ≠ | :x:
| cv25 | `.-` | :x:
| cv26 | `:-` | :x:
| cv27 | `[]` → □ | :x:
| cv28 | `{.` `.}` | :x:
| cv29 | Curly `{` `}` | :heavy_check_mark:
| cv30 | Longer `\|` | :heavy_check_mark:
| cv31 | Curly `(` `)` | :heavy_check_mark:
| cv32 | `.=` | :x:
Legend:
| Icon | Descriptions |
| ------------------ | ------------ |
| :heavy_check_mark: | A character variation. Preserve this.
| :x: | A ligature. Remove this.
| :thinking_face: | Can't decide what to do.
Some explainers:
1. It "undoes" the ligature for `==` and `===` (→ ≡), just making them look tighter. They are still counted as ligatures, but not enabled by default. I would like to preserve the `==` and `===` ones.
The `!=` and `!==` ligatures turn into "≠" but longer. These are definitely not preserved.
2. Fira Mono made two ligatures `fi` and `fl` available via `dlig`, which were [surprisingly](https://github.com/mozilla/Fira/issues/14) 1-char wide! Fira Code removes the "feature", and it fine-tunes letter pairs `f[ij]+` and `[FTI]l+` so that the horizontal strokes are aligned. Ligatures for those letter pairs, still 2-char wide, are available via `ss10`. But if you inspect more carefully, `ss10` actually contains two extra pairs `fl` and `ft`. The problem is that `ft`'s horizontal bar are not aligned ([ref.](https://github.com/tonsky/FiraCode/blob/6.2/extras/ligature_variants.png))
<div style="margin: 20px; padding: 20px; box-shadow: inset 0 0 0 1px currentColor">
<input class="fira-mono-dlig" type="checkbox"> dlig?
<pre style="font-size: 2em">fi fii fl fll</pre>
</div>
<details>
<summary>It should look like this: (not working on my Android 11 phone!)</summary>
![sample when turning on dlig](https://i.imgur.com/3yKQ2vK.png)
</details>
3. Rules can interact with each other, as they are expected to be controlled independently. For instance, there is a ligature for `%%` called `percent_percent.liga`, and since `cv18` substitutes `%`, it oughts to provide a ligature variation `percent_percent.liga.cv18` when both are in effect. So `.liga` may not always appear in the end. There are even duplicates `zero.zero.tosf` and `zero.tosf.zero`!
## Contextual alternates (`calt`)
See `features/calt/*.fea` in repo:
* :ok: `center`: Verticially align `<:>`s, use `.center`
* :warning: `conj_disj`: `/\` → ∧, `\/` → ∨
* :warning: `cross`: Vertically center the `x` in i.e., `1920x1080` and `0xFF` -- [see this thread](https://github.com/tonsky/FiraCode/discussions/1454)
* :warning: `dashes`: `--` → –, `---` → —
* :warning: `equal_arrows`: connected `<===>>`
* :ok: `fi_fl`: Verticially align `fij` + `ij`; `FTIl` + `l`, use `.salt_low`
* :ok: `greek`: Replace `caltGreekUCdiph` and `caltGreekUC`. From Fira Mono; Not knowing what exactly they are
* :warning: `hyphen_arrows`: connected `<--->>`
* :thinking_face: `match_cases`: Vertically align `-+*`s (w/ lc) and `:`s (w/ uc); must go after `hyphen_arrows.fea`, use `.lc` and `.uc`
* causes some glitches, the vertical positions of `-` and `=` not aligning is very annoying! I am disabling it for now...
* I personally encountered some issue for the auto hinter in FreeType under GNOME where hinting mode is set to `slight`. It only appears in specific point sizes.
* :thinking_face: `numbersigns`: Connect `###`s
* :thinking_face: `underscores`: Connect `___`s (shouldn't `_` always appear connected? [They are *invented* for this!](https://en.wikipedia.org/wiki/Underscore))
* These two are tricky... I rarely see reasons to connect `#`s but connecting `_`s seems legit.
Some other features (not exhaustive; some are automatically generated by Glyphs):
* `onum` = oldstyle figures, glyphs suffixed `.tosf`
* `tnum` = tabular figures
* `frac` = fractions (`numr`/`dnom` are deprecated); [Glyphs' implementation](https://glyphsapp.com/learn/fractions) is naive, see [w3c](https://github.com/w3c/font-text-cg/issues/34), also [opentypecookbook.com](https://opentypecookbook.com/common-techniques/), [@sev/frac](https://gitlab.com/sev/frac), for a better receipt, also [Iosevka's implementation](https://github.com/be5invis/Iosevka/blob/main/packages/font-otl/src/gsub-frac.ptl) is the best AFAIK, but its code is cryptic.
> [!WARNING]
> Fira Mono's fraction bar's horizontal position is [wrong](https://github.com/mozilla/Fira/issues/196) (and so is Fira Code):
> <pre style="font-size: 2em; border-left: none">1234⁄5678</pre>
> :::spoiler Reference image
> ![sample result of fraction bar](https://hackmd.io/_uploads/HyiqFnBCR.png)
* `sinf` = scientific inferiors; note that these are different from subscripts
* `subs`/`sups` = subscripts/superscripts
* `salt` = stylistic alternates -- for balancing symbol height in small caps
* `case` = case-sensitive forms (infinity symbol??)
## To customize
### 1. update glyphs
The script `script/update_glyphs.sh` invokes `clojure -M -m fira-code.main`, updating corresponding portions from other source files. When using the master glyph definitions, the output is (excerpted):
```
Parsing 'FiraCode.glyphs'...
generated calt: 55 pairs, 26 triples, 3 quadruples, 84 total
replacing class ClosingBracket with 3 entries
replacing class Digit with 13 entries
replacing class DigitTosf with 12 entries
replacing class HexDigit with 12 entries
replacing class OpeningBracket with 3 entries
replacing class Tall with 16 entries
appending to feature calt 270 lines
replacing feature cv01 with 10 lines
...
replacing feature cv32 with 6 lines
replacing feature onum with 12 lines
replacing feature ss01 with 1 lines
...
replacing feature ss10 with 29 lines
replacing feature zero with 2 lines
regenerated NotSpace: 2022 glyphs
Saving 'FiraCode.glyphs'...
```
The ligature logic is coded in `clojure/fira-code/calt.clj`.
### 2. Recompile
See [here](https://github.com/tonsky/FiraCode#building-fira-code-locally). A quick reference to `script/build.sh` is:
* `-f`/`--features`: Comma-separated list of font features to use. If specified, it calls `script/bake_in_features.sh` first to update all specified features to `features[name=calt]`, which is by default enabled in most environments.
* `-w`/`--weights`: Comma-separated list of weights names to build. Default to build all weights.
* `-n`/`--family-name`: Name of this font family, or "`features`" to use default font name with features suffixed.
* `-g`/`--generate-glyphs-only`: Only generate the updated glyphs file.
The targets are:
* TTF (the default. OTF is not included!)
* Variable font (derived from TTF)
* WOFF2
* WOFF
# Features of Fira Mono
We are not removing all alternative glyphs from Fira Code, only those not presented in Fira Mono. (So we cannot simply use font feature freezers [suggested by Fira Code's official wiki](https://github.com/tonsky/FiraCode/wiki/How-to-enable-stylistic-sets#baking-in-stylistic-sets-into-the-font-file)...)
* `aalt`
* `calt`
* `dlig`
* `onum`/`tnum`/`zero`
* `frac`/`dnom`/`numr`
* `subs`/`sups`
* `ordn`
* `case`
* `locl`
[ref.](https://github.com/bBoxType/FiraSans/blob/master/Fira_Mono_3_2/PDF/FiraMono-Regular.otf.pdf)
Some more cherry picking is happening.
# Diff of Fira Code (5.2 v.s. "fixed")
The repo contains a `fixed` [branch](https://github.com/tonsky/FiraCode/tree/fixed) alongside v5.2. It would save much time if we diff commits and apply changes to the master branch instead of starting over. Essentially, it patches the following:
* Set `export=0` to glyphs for ligatures since they become unused
* Remove OpenType features related to variations of ligatures
* Remove those names in all OpenType classes
## Notable changes
<details style="padding: 10px; border: 1px solid currentColor">
* In `classes`:
* `name=notSpace`, :heavy_minus_sign: `*.liga` and `*.seq`
* `name=Uppercase`, :heavy_minus_sign: `?doubleStruck`
* `name=Lowercase`, :heavy_minus_sign: `w_w_w.liga`
* Note: Among these, Only `notSpace` will be auto-updated by script.
* In `features`:
* `aalt`: :heavy_minus_sign: `ss02`, `ss07`, :heavy_plus_sign: `hwid`
* Delete `liga`, `dlig`
* `calt`: TODO remove lots of `lookup {...}`
* :heavy_plus_sign: `hwid`
```js
({ "hwid": {
"automatic": 1,
"code":
"sub cornerbracketleft by cornerbracketleft.half;\n" +
"sub cornerbracketright by cornerbracketright.half;\n" +
""
}})
```
* Delete `ss02`:
> Less Than/Greater Than with horizontal bar
* `ss03`: remove the line "`sub ampersand_ampersand.liga by ampersand.ss03;`"
* `ss04`: remove "`"sub dollar_greater.liga by ...`" *3
* `ss05`: remove "`sub asciitilde.spacer' asciitilde_at.liga by asciitilde;`" *2
* `ss06`: remove `lookup backslash_thin { ... }`
> Thin backslash
* Delete `ss07`
> Regex matching operator
* Delete `ss08`
> Gaps in double-triple equals
* In `glyphs`:
* edit `w_w_w.liga`
* Add `export: 0` to `*.liga` glyphs (TODO: `.rem`?)
## Maintainance patches
* Edit `familyName` to ~~comply with SIL OFL~~ no RFN issues here, [see here](https://github.com/tonsky/FiraCode/issues/573).
* Edit `customParameters[name=note]` to something reasonable.
</details>
# Colopon
The font for monospaced text of this page is **Fira Mono** taken from [Google Fonts](https://fonts.google.com/specimen/Fira+Mono) (sorry, but web font has to be base64-encoded in data URI because of the CSP).
Here is a stripped Fira Code Glyphs file converted into JSON for reference: https://jsoneditoronline.org/#left=cloud.a35538d8abb04b11afbf7e86ca50ff1a
## References
* OpenType features
* http://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#5
* https://simoncozens.github.io/fonts-and-layout/features.html
* https://learn.microsoft.com/en-us/typography/opentype/spec/ttoreg
* https://github.com/schriftgestalt/GlyphsSDK/blob/Glyphs3/GlyphsFileFormat/GlyphsFileFormatv2.md
* GitLab Mono, GitLab's variation of JetBrains Mono: https://gitlab.com/gitlab-org/frontend/fonts
* See its Makefile: It uses `pyftsubset` to remove the ligatures.
* https://github.com/jenskutilek/FiraSystemFontReplacement
* For older OS X only. Archived.
* Ongoing efforts
* [#1374: OTFeatureFreezer without ligatures](https://github.com/tonsky/FiraCode/issues/1374)
* https://blog.liang2.tw/posts/2022/03/fix-fira-code-font-features/
<script type="happy hacking :)">
<style>
code, pre {
font-family: 'Fira Mono', monospace;
}
._box {
display: inline-block;
vertical-align: middle;
}
.fira-mono-dlig {
height: 20px; width: 20px;
}
.fira-mono-dlig:checked ~ pre {
font-feature-settings: 'liga' 1, 'dlig' 1;
font-variant-ligatures: discretionary-ligatures;
}
</style>
<script type="happy hacking :)">
{%hackmd @q/fira-mono %}