# 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 %}