Try   HackMD

Notes on removing ligatures from Fira Code

to make a drop-in replacement of (now unmaintained) Fira Mono.

"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.

Project update: 2025/3/23
Unfortunately, the glyph file format is updated in this commit. If I want to pick up the work, I need to rebase everything I have changed to this commit

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. So sad.

There is an official branch 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 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/):

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 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?

    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 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):

Code Character(s)
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
?
Notes
ss01 r
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
ss02 <= → ≤; >= → ≥
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
ss03 &
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
ss04 $
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
ss05 @
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
ss06 Thin \ when escaping
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
ss07 =~ → ≈; !~ → ≉
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
ss08 Small gaps in ==-s
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
become void automatically *1
ss09
<<= >>=
|= ||=
to arrow
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
ss10 Connected Tl fl fi
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
keep for bw comp. w/ Fira Mono *2
cv01 a
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
cv02 g
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
cv03..06 i
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
cv07..10 l
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
cv11..13 0 (zero)
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
see also zero
cv14 3
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
cv15..16 *
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
cv17 ~
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
cv18 %
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
*3
cv19..20 <= → (19: ≤; 20: ⇐)
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
cv21..22 =< → (21: ⩽; 22: ≤)
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
cv23 >= → ≥
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
cv24 /= → ≠
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
cv25 .-
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
cv26 :-
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
cv27 [] → □
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
cv28 {. .}
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
cv29 Curly { }
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
cv30 Longer |
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
cv31 Curly ( )
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
cv32 .=
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Legend:

Icon Descriptions
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
A character variation. Preserve this.
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
A ligature. Remove this.
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
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 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.)

    dlig?
    fi fii fl fll
    It should look like this: (not working on my Android 11 phone!)

    sample when turning on dlig

  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:

  • Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    center: Verticially align <:>s, use .center
  • Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    conj_disj: /\ → ∧, \/ → ∨
  • Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    cross: Vertically center the x in i.e., 1920x1080 and 0xFF see this thread
  • Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    dashes: -- → –, --- → —
  • Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    equal_arrows: connected <===>>
  • Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    fi_fl: Verticially align fij + ij; FTIl + l, use .salt_low
  • Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    greek: Replace caltGreekUCdiph and caltGreekUC. From Fira Mono; Not knowing what exactly they are
  • Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    hyphen_arrows: connected <--->>
  • Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    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.
  • Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    numbersigns: Connect ###s
  • Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    underscores: Connect ___s (shouldn't _ always appear connected? They are invented for this!)
    • 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 is naive, see w3c, also opentypecookbook.com, @sev/frac, for a better receipt, also Iosevka's implementation is the best AFAIK, but its code is cryptic.

    Warning

    Fira Mono's fraction bar's horizontal position is wrong (and so is Fira Code):

    1234⁄5678
    Reference image

    sample result of fraction bar

  • 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. 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)

  • aalt
  • calt
  • dlig
  • onum/tnum/zero
  • frac/dnom/numr
  • subs/sups
  • ordn
  • case
  • locl

ref.

Some more cherry picking is happening.

Diff of Fira Code (5.2 v.s. "fixed")

The repo contains a fixed branch 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

  • In classes:
    • name=notSpace,
      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →
      *.liga and *.seq
    • name=Uppercase,
      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →
      ?doubleStruck
    • name=Lowercase,
      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →
      w_w_w.liga
    • Note: Among these, Only notSpace will be auto-updated by script.
  • In features:
    • aalt:
      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →
      ss02, ss07,
      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →
      hwid
    • Delete liga, dlig
    • calt: TODO remove lots of lookup {...}
    • Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →
      hwid
      ​​​​({ "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.
  • Edit customParameters[name=note] to something reasonable.

Colopon

The font for monospaced text of this page is Fira Mono taken from Google Fonts (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