Font ligatures and Unicode symbols¶
1. Why this matters¶
Lex documents lean heavily on arrows and logical symbols. ->, <-, =>, !=, <=>, ... appear constantly — not just in technical specs but in everyday prose: cause and effect, logical implication, ranges, approximate values, ellipses.
Authors writing Lex in a ligature-aware monospace font (Geist Mono, Fira Code, JetBrains Mono) see these ASCII sequences rendered as proper typographic symbols. The source stays ASCII on disk, but the screen shows → ⇒ ≠ ⇔ …
The problem is that rendering is purely local to the author's editor. When the document ships — to a coworker opening the HTML in Chrome, a student viewing the PDF, a reviewer on a different OS — the ligatures vanish. -> becomes a dash followed by a greater-than sign. The visual clarity the author relied on is gone.
Lex is meant to reach beyond technical authoring environments. The reader viewing a Lex-generated HTML file should see the same typographic symbols the author saw, regardless of whether they have a programmer's font installed. This document surveys which ASCII-to-symbol mappings can be done at the text layer (via Unicode substitution) versus which require a controlled font layer (via embedded webfonts with OpenType ligature features).
2. The tiers¶
Symbols split into three tiers based on how reliably they render across fonts. Tier 2 splits further by Unicode block, because the practical coverage story is different for each half.
2.1. Tier 1 — universal¶
Codepoints in the oldest BMP blocks: Arrows (U+2190–U+21FF core range), basic Mathematical Operators (U+2200–U+22FF), General Punctuation (U+2000–U+206F). Present in every mainstream font — OS defaults, Google Fonts text families, bare system fallbacks like DejaVu and Liberation. Safe to substitute unconditionally in prose.
2.2. Tier 2a — widespread¶
Codepoints in the upper Arrows block (U+219C–U+21DD) and extended Mathematical Operators (U+2254, U+2262), plus the interrobang (U+203D). Added in early Unicode versions and carried by all text-optimized fonts: Arial, Helvetica, Roboto, Inter, Open Sans, Lato, Source Sans, Noto Sans. Safe to substitute when targeting mainstream fonts; occasional fallback in older or display-focused typefaces.
2.3. Tier 2b — spotty¶
Codepoints in Supplemental Arrows-A (U+27F5–U+27FA) — the long-form arrows. Coverage is inconsistent. Arial and Helvetica historically do not cover these; the browser falls back to a math-capable font (STIX, Cambria Math), rendering the glyph at a mismatched weight and baseline. Inter, Noto Sans, SF Pro, and Segoe UI cover them cleanly. Substitute only when the font is controlled via @font-face.
2.4. Tier 3 — font-only¶
No reliable Unicode equivalent. These ligatures exist only as font-level OpenType substitutions (calt / liga features). The ASCII sequence must stay as-is in the source; visual rendering is entirely the font's responsibility.
3. Tier 1 substitutions¶
Safe for every output, every font, every reader
| ASCII | Symbol | Codepoint | Name |
|---|---|---|---|
-> |
→ | U+2192 | Rightwards arrow |
<- |
← | U+2190 | Leftwards arrow |
<-> |
↔ | U+2194 | Left right arrow |
=> |
⇒ | U+21D2 | Rightwards double arrow |
<=> |
⇔ | U+21D4 | Left right double arrow |
<= |
≤ | U+2264 | Less than or equal |
>= |
≥ | U+2265 | Greater than or equal |
!= |
≠ | U+2260 | Not equal |
/= |
≠ | U+2260 | Not equal |
=/= |
≠ | U+2260 | Not equal |
=== |
≡ | U+2261 | Identical to |
~~ |
≈ | U+2248 | Almost equal to |
~= |
≃ | U+2243 | Asymptotically equal |
... |
… | U+2026 | Horizontal ellipsis |
-- |
– | U+2013 | En dash |
--- |
— | U+2014 | Em dash |
4. Tier 2a substitutions¶
Safe in any text-optimized font (Arial, Helvetica, Roboto, Inter, Open Sans, Lato, Source Sans, Noto Sans)
| ASCII | Symbol | Codepoint | Name |
|---|---|---|---|
:= |
≔ | U+2254 | Colon equals |
!== |
≢ | U+2262 | Not identical to |
!? |
‽ | U+203D | Interrobang |
->> |
↠ | U+21A0 | Two-headed rightwards |
>-> |
↣ | U+21A3 | Rightwards arrow with tail |
~> |
⇝ | U+21DD | Rightwards squiggle arrow |
<~ |
↜ | U+219C | Leftwards wave arrow |
<~> |
↭ | U+21AD | Left right wave arrow |
<<- |
↞ | U+219E | Two-headed leftwards |
One additional Tier 2a mapping is worth noting but sits outside the table because | is the Lex cell delimiter: the bar-hyphen-greater sequence (written in ASCII as pipe followed by ->) maps to ↦ (U+21A6, Maps to) — the functional "mapsto" arrow common in type signatures and mathematical writing.
5. Tier 2b substitutions¶
Use only when an embedded webfont with Supplemental Arrows-A coverage is guaranteed. Without that guarantee, these render in a fallback font and look visually worse than plain ASCII
| ASCII | Symbol | Codepoint | Name |
|---|---|---|---|
--> |
⟶ | U+27F6 | Long rightwards arrow |
<-- |
⟵ | U+27F5 | Long leftwards arrow |
==> |
⟹ | U+27F9 | Long rightwards double arrow |
<== |
⟸ | U+27F8 | Long leftwards double arrow |
<==> |
⟺ | U+27FA | Long left right double arrow |
6. Tier 3 — font-only¶
No reliable Unicode equivalent. Leave as ASCII and rely on an embedded ligature font (or on the reader having one installed) if you want the glyph rendering.
- Representative examples
-
==,=>>,=<<>=>,>>=,>>-,>--<,-<<,<=<,<<=,<-<<|,<||,<|||,<|>,|||>,||=,||>,|>,|=|},{|,</>,<!--,<>~@,~-,-~,~~>,<~~###,%%,.=,..=,??,???
7. Font coverage comparison¶
Coverage of each tier across common font families. ✓ = reliable coverage, ~ = partial (browser fallback likely for some codepoints), ✗ = no coverage, liga = rendered via OpenType liga / calt rather than via dedicated Unicode codepoints
| Font family | Tier 1 | Tier 2a | Tier 2b | Tier 3 |
|---|---|---|---|---|
| Arial | ✓ | ✓ | ✗ | ✗ |
| Helvetica / Helvetica Neue | ✓ | ✓ | ✗ | ✗ |
| Times New Roman | ✓ | ✓ | ✗ | ✗ |
| Courier New | ✓ | ~ | ✗ | ✗ |
| SF Pro (macOS system) | ✓ | ✓ | ✓ | ✗ |
| Segoe UI (Windows system) | ✓ | ✓ | ✓ | ✗ |
| Roboto | ✓ | ✓ | ~ | ✗ |
| Inter | ✓ | ✓ | ✓ | ✗ |
| Open Sans | ✓ | ✓ | ~ | ✗ |
| Lato | ✓ | ✓ | ~ | ✗ |
| Source Sans 3 | ✓ | ✓ | ~ | ✗ |
| Noto Sans | ✓ | ✓ | ✓ | ✗ |
| Merriweather / Playfair (serif display) | ✓ | ~ | ✗ | ✗ |
| Fira Code | ✓ | ✓ | ✓ | liga |
| JetBrains Mono | ✓ | ✓ | ✓ | liga |
| Geist Mono | ✓ | ✓ | ✓ | liga |
| Cascadia Code | ✓ | ✓ | ✓ | liga |
- A few notes on reading the table
-
- The
~cells are the trap. The glyph renders, but not in the requested font — the browser silently swaps to a math fallback with a different weight and baseline. Visually it's worse than leaving the ASCII alone. - The display serifs row illustrates why typeface choice matters beyond aesthetics: a heading font that looks beautiful for titles may fail on body text that contains Tier 2 symbols.
- Monospace ligature fonts are the only place Tier 3 sequences render as symbols, and they do it via OpenType features, not Unicode substitution.
- The
8. Recommendation¶
Two layers, applied together:
- Text layer (Unicode substitution pass in the HTML/PDF formatter, applied to prose only — never inside verbatim blocks or inline
code) -
- Always apply Tier 1.
- Apply Tier 2a when the document's font stack leads with a widespread text font or embedded webfont.
- Apply Tier 2b only when an embedded webfont with Supplemental Arrows-A coverage is guaranteed via
@font-face. - Never substitute Tier 3. Leave ASCII; let the font do the work or let it stay literal.
- Font layer (for readers who want full ligature fidelity regardless of their system)
-
- Embed a ligature-capable font via
@font-facein HTML output. Geist Mono and JetBrains Mono are both OFL-licensed and cover Tier 3 cleanly. - Enable
font-feature-settings: "liga", "calt";on code and verbatim blocks. - This closes the Tier 3 gap and covers any Tier 2 misses without source transformation.
- Embed a ligature-capable font via
- Config surface proposal
-
unicode_substitutions = "basic" | "extended" | "full" | "off"basicapplies Tier 1 only (default).extendedadds Tier 2a.fulladds Tier 2b (requires embedded font guarantee).offdisables substitution entirely; ASCII ships as written.
The guiding principle: substitution should never produce a worse rendering than the ASCII it replaces. Tier 1 never does. Tier 2a rarely does. Tier 2b will, unless the font is pinned. Tier 3 always does.