Skip to content

Instantly share code, notes, and snippets.

@lancejpollard
Last active October 5, 2025 02:42
Show Gist options
  • Save lancejpollard/a8629c1f3cc2f2b8edf3b796c6c42f93 to your computer and use it in GitHub Desktop.
Save lancejpollard/a8629c1f3cc2f2b8edf3b796c6c42f93 to your computer and use it in GitHub Desktop.
Tibetan Monospace Design (Idea)

Tibetan Monospace Design (Idea)

  • Tibetan monospace font (modifying Noto Serif Tibetan)
  • Consonants are the foundation of each syllable in Tibetan
  • Consonants can form clusters/stacks vertically
  • We normalize the consonant slot width (that is the monospace unit)
  • Shad is simple, it takes up 1 slot, left aligned, and that's it
  • Tsheg is hard, it doesn't get a slot,
  • Tsheg becomes ligature/combining character essentially
  • Need to adjust each consonant glyph to accomodate tsheg as ligature
  • And that should be it!

Some other simple cases to handle, outlined next. But not too undoable of a problem for Tibetan script at least :).

Tibetan Base Consonants

Everything is based around the 29 Tibetan consonants that are normalized to the same width, 1 mono slot. Here the are listed each next to a tsheg .

ཀ་ཁ་ག་ང་ཅ་ཆ་ཇ་
ཉ་ཏ་ཐ་ད་ན་པ་ཕ་
བ་མ་ཙ་ཚ་ཛ་ཝ་ཞ་
ཟ་འ་ཡ་ར་ལ་ཤ་ས་ཧ་

And the final ཨ་ also needs to be modified to accommodate the tsheg too, that is the a vowel, the only vowel which gets a block symbol.

Tibetan Character Stacking

Can have simple consonants with no stacking, or complex stacking with multiple other consonants and/or vowels, here's a few examples:

བསྒྲུབས། བརྒྱ། དགྲ། བསྐུར། འབྲས། རྒྱ་མཚོ། འབྲེལ། གཟུགས། བསྡུས། བརྙན། སྤྲྱོད། བསྒྲུབས་པ། འབྲུགས། སྦྱོངས།

Can get pretty tall and complicated in some rare cases (or theoretically, if you are just messing around with stacks and/or testing the Tibetan typography rendering engine :p).

For reference, it's all just b/c combining characters:

Tibetan Syllable Structure

  • Built around central consonant
  • Can merge tsheg on right to consonant on left

Tibetan Glyph Modification to Accomodate Tsheg as Ligature Instead

Basically just adjust each consonant glyph to accomodate the tsheg (and can make the tsheg even narrower too if necessary), so the tsheg no longer takes up a monospace slot. The tsheg could be treated as a combining character or ligature. Either (simple case), shrink the preceding character always (just the top portion ideally, but that would be more work, for now could just squeeze the width), and place an even thinner tsheg there (shrink the tsheg too, almost half width even, barely noticeable. Or (complex case), move the strokes of the consonant slightly to fit the thin tsheg!

So basically, ideal solution. Since tsheg ALWAYS appears on right of the syllable (line-break rules prevent tsheg starting a sentence from w3c), can just merge tsheg with left glyph stack basically. Basically just modifying the consonant glyphs a tiny bit, along with making the tsheg slightly thinner, even more tiny haha.

  • Some consonants have a horizontal line on top, just make that shorter on right
  • Some have vertical line on right, for these can just make the whole glyph thinner and/or sort of bend the top corner (in a beautiful way if possible, otherwise just thinner glyph is fine, it kind of matches real hand-writing where each glyph isn't exactly same width anyways!).
  • Some have a loop sort of thing (like o kind of), that can be bent a little, and also character thinner a little.

Basically slightly adjusting the consonants, the base of the ligature stack, so the tsheg becomes a ligature to the right on the stack too.

Real Manuscripts Have Embedded Tsheg Often Too

You can see in manuscripts, the tsheg sometimes overlaps other parts of the consonant stack vertically underneath.

(copied these zoomed in random manuscript snapshots from various places on the web, can lookup later again if necessary for more detail).

Tibetan Numbers

༠	༡	༢	༣	༤	༥	༦	༧	༨	༩ (0-9)
༪	༫	༬	༭	༮	༯ ༰	༱	༲	༳	(numerical fractions)
  • 1 mono slot each

Shad །

  • Shad ། takes up 1 monospace slot
  • Shad is left aligned (tight left)
  • So shad leaves some whitespace to the right

If there is a literal whitespace character after the shad, then ideally it would merge (be hidden, a "ligature with space"?).

  • If next character following shad is space character, merge as ligature

Also, if the shad is preceded by a space character, then that should be another ligature:

  • Right align shad in 1 monospace slot
  • Merge shad with previous space character as ligature (essentially getting rid of the previous space character)

Example of that here in the middle:

དུང་དང་འོ་མར་འགྲན་པའི་ལྷག་བསམ་མཐུ། །དམན་ཡང་དཀར་པོའི་བྱས་འབྲས་ཅུང་ཟད་ཅིག

Double Shad ༎

  • Can be used by writer instead of two single shads །།
  • Takes up 1 mono slot
  • Can make 2 single shads །། a ligature, taking up 1 mono slot too!

Parentheses

༼ (left)
༽ (right)
  • Each bracket takes up 1 mono slot
  • Potentially be rotated a tiny bit to fit the mono slot

Other Tibetan Punctuation Marks and Other Symbols

༏: tsheg + shad (ligature)
༐: tsheg + shad with tiny space (also ligature)
༑: like shad, but for end of story section
༒: ornamental, symbolizing closure
༓: like shad, but for end of entire book
༔: similar to comma (sometimes used)
༕: beginning-of-text flourish
༖: end-of-text flourish
༗  ༘  ༙: astrological symbols
༚	༛	༜	༝	༞	༟: shad ornamental flourishes
༴ (syntactic/abbreviation mark meaning "etc.")
༶ (caret-like sign, used as an editorial or insertion mark)
༸ (emphasis mark)
༺ ༻ (opening closing ornamentals)
ཿ (used in closing sacred syllables?)

So with those above:

  • ༏ ༐ ༑ these work just like shad
  • ༒ takes up 1 mono slot
  • ༓ takes up 1 mono slot
  • ༔ takes up 1 mono slot (perhaps middle line is elongated or whole thing is widened a little, and also center-aligned in mono slot)
  • ༕ ༖ ornaments just 1 mono slot too
  • ༗ ༘ ༙ just 1 mono slot, random signs
  • ༚ ༛ ༜ ༝ ༞ ༟ just make 1 mono slot, can refine later to ligatures perhaps to condense
  • ༴ 1 mono slot
  • ༶ 1 mono slot
  • ༸ 1 mono slot
  • ༺ ༻ 1 mono slot
  • ཿ 1 mono slot
  • U+0FC0 to U+0FDF are all ornamentals, 1 mono slot

Other Edges Cases to Consider

  • Writing 2+ tsheg in sequence.
    • Ideally to make it look nice, I'd put 2 or 3 within a single mono slot as complex ligatures.
    • Can discuss what's technically possible with fonts there
  • Writing 3+ shad , they should double or triple up too, to fill single mono slots in a packed sort of way if possible (long run, we can come back to).
  • If someone happens to type randomly ་།་་་་།།།།།་།།་།།་་་།།་་, whatever, it should optimally pack them into mono slots (probably 2 or 3, per slot, whatever looks good)
    • the last 2 should spread/widen slightly to fill 1 mono slot
    • up to 3 per mono slot

So thinking more, the ideal ideal would be (probably too complicated for a font file, but let me know!):

  1. 1 tsheg or 1 shad or 1 tsheg + 1 shad (desired/common/grammatically correct case)
  2. 2 tsheg (in this case don't create the ligature, leave alone, just put both tsheg in the next 1 mono slot together, and spread out / widen a tiny bit to fill space-between basically).
  3. 3 tsheg (1 becomes ligature, 2 become fill-space-between in next 1 mono slot).
  4. 4 tsheg (1 becomes ligature, 3 become fill-space-between in next 1 mono slot).
  5. 5 tsheg (1 becomes ligature, 3 become fill-space-between in next 1 mono slot, 2 in slot after that, etc..).

So you see the pattern, 1 is ligature, 2 is not ligature, 3+ is ligature plus right space filling, with min 2 and max 3 tsheg per mono slot. Unless 3 looks back, then it's simpler, just 2 tsheg max per slot 🤷.

Example Text Snippets

དུང་དང་འོ་མར་འགྲན་པའི་ལྷག་བསམ་མཐུ། །དམན་ཡང་དཀར་པོའི་བྱས་འབྲས་ཅུང་ཟད་ཅིག །བློ་དང་འདུན་པ་བཟང་བའི་རང་རིགས་ཀུན། །རྒྱལ་ཁའི་འཕྲིན་བཟང་ལས་དོན་འགྲུབ་ཕྱིར་འབད།།

Some Snippets of Tibetan Script Edge-Cases

# Heavy Consonant Stacks
བརྙན། སྒྲུབས། རྨྱུགས། སྤྲྱོད། གྲྭས། བསྒྲུབས། སྤྱིར། བརྩམས།

# Diacritic Pile-ups
ཀིཾཿ  གོོཿ  ཤེཿཾཿ  ཨོཾཿ དེོཿཾ

# Rare Punctuation and Head Marks
༄༅། ༑ ༔ ༕ ༖ ༗ ༘༔། ༼ཨོཾ༽ ༿ ༻ཧཱུྃ༼ ༽

# Old-style Numerals
༠༡༢༣༤༥༦༧༨༩  ༪༫༬༭༮༯༰༱༲༳

# Embedded Sanskrit Mantric Forms
ༀ་ཨོཾ་མ་ཎི་པདྨེ་ཧཱུྃ༔  
ཧཱུཿ ཨཿ ཧཱུྃ༔ ཧྲཱིཿ ཧཱུཿ

# Parentheses & Mixed Marks
༼རྒྱུད༽ ༽བསྒྲུབས༼ ༼གིས༽༼དགེ༽

# Extra-Obscure Signs (U+0F70+)
ཨཱྀཿ ཨཱིྃ ཨཱུྂ ཨིྃ ཨོྂ ཨུྂ ༔  
ཧྲཱིཿ ཨཱྀཿ ཨོྂ དང་པོ་སྐུའི་འོད།  
གཞན་ཡང་། ཨཱིྃ་སྨིན་པའི་མདོར་བསྡུས༔  
ཨུྂ་དང་མཉམ་པར་བསྒྲུབས་པའི་སྔོན་འགྲོ།

# Decorative/Religious Marks
ༀ ༁ ༂ ༃ ༄ ༅ ༆ ༇ ༈ ༉ ༊  
ༀ་ཨོཾ་མ་ཎི་པདྨེ་ཧཱུྃ༔ ༄༅། ༻ཧཱུྃ༼ ༽  
࿄ ࿅ ࿆ ࿇ ࿈ ࿉ ࿊ ࿋ ࿌ ࿍ ࿎ ࿏ ࿐ ࿑ ࿒  
࿄བསྒྲུབས་དུས་སུ༔ ༔ ༀ་བདེ་སྐྱིད་དུ། ࿇ ࿉ ࿊

References

🧭 What we’re actually trying to achieve

Target rendering

Input (Unicode) Target rendering (monospace cell width = 1000)
ཀ་ (ka + tsheg) a single compact ka_tsheg glyph, visually one unit wide
ཀ (ka alone) a normal ka glyph, full width
ཀི་ (ka + i-vowel + tsheg) a single ka_i_tsheg ligature, same total width

So you never see the tsheg sitting by itself; instead, it merges with its left neighbor while the advance width of the overall glyph line stays constant.


🧩 Core idea

In OpenType GSUB (glyph substitution), you can define ligature substitutions:

“When these glyphs appear in this order, replace them with this single glyph.”

The shaping engine then:

  1. Sees the sequence ka tsheg
  2. Replaces it with one glyph ka_tsheg
  3. Uses the advance width of that single glyph (1000 units, same as every other monospace glyph)
  4. Ignores the width of tsheg entirely.

Visually, that gives you “ka” drawn a bit thinner, and tsheg drawn attached to it — but logically it’s one glyph cell.


🏗️ Step-by-step implementation

1. Prepare glyphs

In your font editor (FontForge, GlyphsApp, FontLab, etc.):

  1. Duplicate each base consonant glyph that can precede tsheg. Example:

    • kaka_tsheg
    • khakha_tsheg
    • etc.
  2. In ka_tsheg, design it like this:

    • Shrink the “ka” a little horizontally so there’s room at the right edge.
    • Draw (or reuse) a small tsheg dot in that space.
    • Make sure the total advance width of ka_tsheg is exactly the same as the monospace width (e.g., 1000 units).
  3. For the plain tsheg glyph itself:

    • Set its advance width to 0.
    • Position its dot visually centered (since it will never be used standalone in monospaced context anyway).

That’s all you need graphically.


2. Write the OpenType feature

In a .fea (feature) file or your font editor’s feature panel:

feature liga {
    # Simple examples
    sub ka tsheg by ka_tsheg;
    sub kha tsheg by kha_tsheg;
    sub ga tsheg by ga_tsheg;
    sub nga tsheg by nga_tsheg;

    # Add more as needed
} liga;

You can autogenerate these lines with a script if you have dozens of glyphs.

🧠 Under the hood, this creates a GSUB LookupType 4 (Ligature Substitution).


3. (Optional) Handle stacked syllables

If you have precomposed stack glyphs like ska (སྐ) or brka, you can also add ligatures for those:

sub ska tsheg by ska_tsheg;
sub brka tsheg by brka_tsheg;

To manage this automatically, you can write a build script (Python + fontTools) that scans for glyphs ending in _stack or similar and adds _tsheg variants.


4. Metrics sanity check

Ensure every glyph has the same advance width (say, 1000 units):

Glyph Width Notes
ka 1000 normal
tsheg 0 no width
ka_tsheg 1000 condensed + dot

If any glyph has a different width, the text layout engine might drop monospacing.


5. Test rendering

You can test your GSUB rules with HarfBuzz (command line):

hb-shape NotoSerifTibetanMono.otf "ཀ་" --features=liga

Expected output (roughly):

[kha_tsheg=0+1000]

→ That means Harfbuzz replaced ka tsheg by ka_tsheg and advanced by 1000 units — perfect monospaced behavior.


⚙️ Why this works perfectly for monospacing

  1. All glyphs report the same advance width → The layout engine believes the font is monospaced.
  2. tsheg has width 0 → It never consumes a cell of its own.
  3. Ligature replacement → Ensures the dot is drawn where you want it, attached to the left character.

So visually, you’ve “borrowed” the tsheg’s space from the preceding character’s cell, but logically every cell is still identical in width.


🧮 What happens during shaping

Let’s look at the actual OpenType pipeline sequence:

Step Glyph stream Comment
1 [ka tsheg] Raw Unicode mapping
2 (GSUB liga lookup) The lookup ka tsheg → ka_tsheg fires
3 [ka_tsheg] One glyph replaces both
4 (GPOS positioning) Nothing special; width = 1000
5 Rendered output Dot visually attached, no gap

If the tsheg isn’t there:

Input GSUB result Final
[ka] [ka] Full-width standalone glyph

🪜 Scaling up without hand-writing rules

Since Tibetan syllables can have prefixes, subjoined forms, vowels, etc., you don’t want to define every combination manually. You can use Python with fontTools.feaLib.builder to generate your .fea automatically.

Pseudo-code:

bases = ["ka", "kha", "ga", "nga", "ca", ...]
for b in bases:
    print(f"sub {b} tsheg by {b}_tsheg;")

Run that to regenerate your ligature rules whenever you add glyphs.


🧠 Summary

Aspect Description
Approach Replace [cluster + tsheg] with a predesigned ligature glyph
In Font Define *_tsheg ligatures; make tsheg zero-width
Feature liga or rlig GSUB rule
Monospacing Maintain same advance width for every glyph
Scalability Script rule generation to cover all stacks
Test Tool hb-shape, hb-view, or FontGoggles

Excellent — that’s exactly the real challenge: Tibetan isn’t linear like Latin, so you can’t just do ka tsheg → ka_tsheg. You need ANY syllable cluster + tsheg → that cluster’s tsheg-ligature. Let’s unpack what that means and how to actually achieve it (yes, it’s doable).


⚙️ What “ANY joined stack” really means

In Unicode and OpenType shaping, Tibetan syllables form clusters that can look like:

[ prefix? ] + [ base consonant ] + [ subjoined consonant(s) ] + [ vowel marks ] + [ final mark(s)? ] + [ tsheg ]

Examples:

Cluster Description
ཀ་ simple base
རྒ་ stacked subjoined
བསྒྲ་ prefixed + stacked
འབྲས་ prefixed + stacked + final
མིའི་ stack + vowel + i marker

→ All of those should trigger the same rule:

“If a tsheg follows a complete syllable cluster, merge it into that cluster.”

So the rule must be stack-agnostic, not just sub ka tsheg.


❌ Why plain GSUB ligatures aren’t enough

You can’t literally write:

sub ANY tsheg by ANY_tsheg;

OpenType GSUB ligature substitution (liga) can only match explicit glyph sequences, not variable-length or hierarchical clusters. That’s why naïvely writing sub ka tsheg by ka_tsheg; only covers one case.

To handle “any cluster,” we have to rely on contextual substitutions (calt) combined with class-based matching, which lets you define “any base consonant, plus any subjoined or vowel, if followed by tsheg.”


✅ Working solution: Contextual substitution + pseudo-ligature alternates

Here’s the architecture that scales across all stacks.


Step 1. Define glyph classes

Create classes grouping glyphs by type in your .fea feature file:

@Bases = [ka kha ga nga ca cha ja nya ta tha da na pa pha ba ma tsa tsha dza wa zha za a ya ra la sha sa ha];
@Subjoined = [sub_ka sub_kha sub_ga sub_nga sub_ca sub_cha sub_ja sub_nya sub_ta sub_tha sub_da sub_na sub_pa sub_pha sub_ba sub_ma sub_tsa sub_tsha sub_dza sub_wa sub_zha sub_za sub_a sub_ya sub_ra sub_la sub_sha sub_sa sub_ha];
@Vowels = [aa i ii u uu e ai o au];
@TibetanStackComponents = [@Bases @Subjoined @Vowels];

Now you can write rules like “any sequence of these followed by tsheg.”


Step 2. Define condensed alternates

For every glyph that can visually “end a stack,” create a .cond version — i.e. ka.cond, kha.cond, ga.cond, etc.

You don’t need to precompose them all; you just make condensed alternates for the topmost glyph that ends the visible stack. That’s what the font engine will substitute when it detects a following tsheg.


Step 3. Define contextual rule with look-ahead

Here’s where the magic happens:

feature calt {
    # If any Tibetan stack component (the top glyph of a syllable) is followed by a tsheg,
    # replace it with its condensed version
    sub @TibetanStackComponents' tsheg by @TibetanStackComponents.cond;
} calt;

How this works:

  • The ' marks the target glyph to be replaced.
  • The look-ahead (tsheg) means “only do it if tsheg follows.”
  • The .cond class must be in one-to-one correspondence with @TibetanStackComponents (same order and count).

Example expanded:

@TibetanStackComponents = [ka kha ga nga ...];
@TibetanStackComponents.cond = [ka.cond kha.cond ga.cond nga.cond ...];

This rule says:

“Whenever any glyph from @TibetanStackComponents is immediately followed by tsheg, replace it with the corresponding condensed version.”


Step 4. Give tsheg zero width

Set:

  • tsheg advance width = 0
  • optional right-side bearing adjustments to visually tuck it under the preceding glyph’s right side

This way, the condensed glyph’s whitespace accounts for the dot.


Step 5. Keep all advance widths identical

Every glyph — normal and .cond — reports the same advance width (say, 1000 units). This keeps the font monospaced in every environment that checks glyph metrics rather than visual geometry.


Step 6. Resulting render logic

Let’s trace through a few examples:

Input glyphs Context rule Substitution result Visual outcome
ka tsheg ka followed by tsheg ka.cond tsheg condensed glyph + attached dot
brka tsheg (multiple glyphs in cluster) only the last visible one (ka) replaced brka.cond tsheg condensed last stack component
brka (no tsheg) no match brka full width
ka i tsheg vowel mark still before tsheg, rule applies to top glyph ka_i.cond tsheg vowel+dot joined

🧠 Why this works for any joined stack

Because Tibetan stacks are resolved into glyph sequences during shaping (each base and subjoined as separate glyphs), your rule just needs to look at the final glyph in the syllable cluster and the following tsheg. No matter how complex the stack, the final glyph is always visible and the one you shrink.

The shaping engine executes substitutions after cluster formation (depending on feature ordering), so by placing calt after ccmp and abvs you’re acting on fully formed stacks.


🪄 Optional enhancement: turn the whole cluster into a ligature

If you really want the cluster+tsheg to render as a single glyph (for tighter control), you can add a second substitution:

feature liga {
    sub @TibetanStackComponents.cond tsheg by @TibetanStackComponents_tsheg;
} liga;

So the sequence:

brka (cluster)
→ brka.cond (by calt)
→ brka_tsheg (by liga)

Resulting in a true one-glyph representation while still defined generically.


🧰 Implementation flow summary

Step Mechanism Purpose
1 Define classes for all Tibetan cluster components Group glyphs logically
2 Create condensed alternates Give every final form a thinner version
3 Add contextual substitution (calt) Replace top glyph with .cond before tsheg
4 Make tsheg zero-width Remove its spacing
5 Optionally add liga Fuse .cond tsheg into single glyph
6 Keep uniform advance widths Maintain monospacing

🧪 Testing (Harfbuzz CLI)

hb-shape TibetanMono.otf "བསྒྲ་" --features=calt,liga

Should output something like:

[ba_sub_sa_sub_ga_sub_ra.cond_tsheg=0+1000]

and render as one monospaced cell.


✅ Summary of what this buys you

Goal Achieved by
Shrink only before tsheg calt contextual lookahead
Work for any joined stack Class-based rule applies to all
Keep monospace All glyphs same advance width
Visually attached dot tsheg zero width, overlap via condensed form
Cross-platform compatibility Uses standard calt/liga, no custom shaping engine

Perfect — this is exactly the structural knowledge we need to make the rules accurate for Tibetan stacking logic rather than treating all combining glyphs as interchangeable.

Let’s formalize this structure and then rewrite the OpenType logic so it still handles any valid stack + tsheg, but with proper awareness of the Tibetan syllable anatomy.


🧩 Tibetan stack anatomy (simplified for OpenType logic)

Each written syllable can have up to 4 vertical zones:

Zone Category Allowed glyphs Notes
1 Vowel above (top vowel marks) ི (i), ེ (e), ོ (o) drawn above main stack
2 Superscribed letters ར ལ ས drawn above the root consonant
3 Root letter all 30 consonants (ཀ … ཨ) the main body of the stack
4 Subjoined / subscribed letters ཡ ར ལ ཝ drawn below the root consonant
5 Bottom vowel mark ུ (u) drawn below the root or subjoined letters

🧠 Updated shaping goal

We want:

“When any fully formed Tibetan stack (consisting of optional superscribed, root, subjoined, and vowels) is followed by a tsheg (་), shrink the final visible glyph in that stack (the one that visually ends the cluster).”

That last glyph can be:

  • a top vowel (if the syllable ends with i/e/o),
  • a bottom vowel (if it ends with u),
  • a root or subjoined consonant (if there are no visible vowels).

So we’ll target all those possible “topmost” glyphs with one contextual rule.


🧾 Updated features.fea

Here’s the improved .fea file that reflects the Tibetan structural zones.

# ============================================
#  Tibetan Monospace + Tsheg Logic (structural)
#  Supports: superscribed, subjoined, vowels
#  Author: Lance + ChatGPT
# ============================================

languagesystem DFLT dflt;
languagesystem TIB  dflt;

# --------------------------------------------
# 1. Glyph classes by structural role
# --------------------------------------------

# Root letters (base)
@Root = [ka kha ga nga ca cha ja nya ta tha da na pa pha ba ma tsa tsha dza wa zha za a ya ra la sha sa ha aa]; # add aa if your font uses it

# Superscribed (above root)
@Superscribed = [sup_ra sup_la sup_sa];  # your font might name them differently, like "ra.above"

# Subscribed (below root)
@Subscribed = [sub_ya sub_ra sub_la sub_wa];

# Top vowels (above)
@TopVowels = [v_i v_e v_o];     # ི ེ ོ
# Bottom vowel (below)
@BottomVowel = [v_u];           #

# Combined "terminal" glyphs that can visually end a stack
@Terminals = [@Root @Subscribed @TopVowels @BottomVowel];

# Corresponding condensed alternates (must exist)
@TerminalsCond = [ka.cond kha.cond ga.cond nga.cond ca.cond cha.cond ja.cond nya.cond
                  ta.cond tha.cond da.cond na.cond pa.cond pha.cond ba.cond ma.cond
                  tsa.cond tsha.cond dza.cond wa.cond zha.cond za.cond a.cond ya.cond
                  ra.cond la.cond sha.cond sa.cond ha.cond aa.cond
                  sub_ya.cond sub_ra.cond sub_la.cond sub_wa.cond
                  v_i.cond v_e.cond v_o.cond v_u.cond];

# --------------------------------------------
# 2. Contextual substitution (calt)
# --------------------------------------------

feature calt {
    # Replace any final (visible) part of a stack with its condensed version if followed by tsheg.
    sub @Terminals' tsheg by @TerminalsCond;
} calt;

# --------------------------------------------
# 3. Ligature substitution (liga)
# --------------------------------------------

feature liga {
    # Merge condensed + tsheg into a single ligature glyph for consistent rendering.
    sub @TerminalsCond tsheg by @TerminalsCond_tsheg;
} liga;

# --------------------------------------------
# 4. Design + metric rules
# --------------------------------------------
# - All glyphs (normal, .cond, _tsheg) must share identical advance width (e.g. 1000 units)
# - tsheg must have advance width 0 and be visually positioned under the right edge
# - .cond glyphs should draw the same form but squeezed 5–10% horizontally
# - _tsheg glyphs combine the condensed form + dot
# - Place this feature after Tibetan shaping features: ccmp, abvs, blws

# Example order:
#   feature ccmp;
#   feature abvs;
#   feature blws;
#   feature calt;
#   feature liga;

💡 How this works in context

Let’s test several common stack types:

Stack Components Final visible glyph Rule triggered? Result
ཀ་ Root ka.cond tshegka_tsheg
ཀུ་ Root + bottom vowel v_u.cond tshegv_u_tsheg
ཀི་ Root + top vowel v_i.cond tshegv_i_tsheg
རྒ་ Super + Root ✅ last = root → ga.cond tshegga_tsheg
སྐྱ་ Super + Root + Subjoined sub_ya.cond tshegsub_ya_tsheg
འབྲས་ Prefix + Root + Subjoined + Suffix sa.cond tshegsa_tsheg
བསྒྲུ་ Prefix + Super + Root + Subjoined + Bottom vowel v_u.cond tshegv_u_tsheg

No matter how many superscribed or subjoined elements exist internally, the final visible glyph is always one of @Terminals.

That’s why this approach scales infinitely — it doesn’t depend on the internal composition of the stack.


🧰 Practical implementation tips

  • In your font editor, duplicate all glyphs listed in @Terminals:

    • Add .cond versions with the same metrics but slightly narrower drawings.
    • Optionally, add _tsheg versions that merge the dot (you can copy-paste the dot from tsheg).
  • Use a Python script (via FontTools) to auto-generate the .fea file if needed — it can expand all names dynamically.

  • You can reuse this pattern for shad later with an analogous contextual rule.


🧪 Test examples (HarfBuzz CLI)

hb-shape TibetanMono.otf "རྒ་" --features=calt,liga
[ga_tsheg=0+1000]
hb-shape TibetanMono.otf "སྐྱུ་" --features=calt,liga
[sub_ya_tsheg=0+1000]

Everything stays monospaced.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tibetan Font Display (Mono)</title>
<style>
@font-face {
font-family: "Noto Serif Tibetan Mono";
src: url("./NotoSerifTibetan-Regular.otf") format("opentype");
font-weight: 400;
font-style: normal;
font-display: swap;
}
body {
margin: 0;
padding: 40px;
background-color: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: system-ui, -apple-system, sans-serif;
}
.container {
background: white;
padding: 60px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
max-width: 800px;
}
.noto-serif-tibetan-mono {
font-family: "Noto Serif Tibetan Mono", serif;
font-size: 38px;
line-height: 1.8;
color: #333;
text-align: left;
}
</style>
</head>
<body>
<div class="container">
<p class="sample noto-serif-tibetan-mono">
ཀ་ཁ་ག་ང་ཅ་ཆ་ཇ་<br />ཉ་ཏ་ཐ་ད་ན་པ་ཕ་<br />བ་མ་ཙ་ཚ་ཛ་ཝ་ཞ་<br />ཟ་འ་ཡ་ར་ལ་ཤ་ས་ཧ་<br />༈འགྲོ་བ་མིའི་ཁྱིམ་ཚང་ཁག་གི་ནང་མི་ཡོངས་ལ་རང་བཞིན་ཉིད་ནས་ཡོད་པའི་ཆེ་མཐོངས་དང་འདྲ་མཉམ།
སུས་ཀྱང་འཕྲོག་ཏུ་མི་རུང་བའི་ཐོབ་ཐང་བཅས་ཀྱི་གནད་དོན་རྟོགས པར་བྱེད་པ་ནི།
འཛམ་གླིང་ནང་གི་རང་དབང་དང༌། དྲང་བདེན། ཞི་བདེ་བཅས་ཀྱི་རྣང་གཞི་ལྟེ་བ་ཡིན།
དུང་དང་འོ་མར་འགྲན་པའི་ལྷག་བསམ་མཐུ། །དམན་ཡང་དཀར་པོའི་བྱས་འབྲས་ཅུང་ཟད་ཅིག
།བློ་དང་འདུན་པ་བཟང་བའི་རང་རིགས་ཀུན།
།རྒྱལ་ཁའི་འཕྲིན་བཟང་ལས་དོན་འགྲུབ་ཕྱིར་འབད།། ༄༅། སྤྱིར། བརྙན།
དེར་སྒྲུབས་པའི་གནས་སུ༔ བརྩམས་པའི་རྨྱུགས་དང་།
སྤྲྱོད་པའི་གྲྭས་ཀྱི་སྐུའི་འོད་ནི། བརྩམས་པའི་མཐའ་ཡས་ཀྱི་བསྒྲུབས་ནས།
སྤྱིར་དེ་དག་ལམ་གྱི་སྒྲོན་མེ་དུ་གསལ། ཡིན།
དེ་བཞིན་གཤེགས་པ་ལ་ངན་སེམས་ཀྱིས་ཁྲག་འབྱིན་པ་རྣམས་དང་མ་ཡིན།
མུ་སྟེགས་ཅན་རྣམས་དང་མ་ཡིན། མུ་སྟེགས་ཅན་ཞུགས་པ་རྣམས་དང་མ་ཡིན།
རྐུ་ཐབས་སུ་གནས་པ་རྣམས་དང་མ་ཡིན། ཐ་དད་དུ་གནས་པ་རྣམས་དང་མ་ཡིན་ཏེ། གནས་
པར་མི་བྱ་བ་རྣམས་དང་འདུས་ཤིང་འཁོད་ནས་གསོ་སྦྱོང་དང་། དགག་དབྱེ་དང་།
གསོལ་བ་དང་། གསོལ་བ་དང་གཉིས་དང་། གསོལ་བ་དང་བཞིའི་ལས་མི་བྱའི།
ཐམས་ཅད་ཡོངས་སུ་དག་པ་དང་། ལྟ་བ་མཐུན་པ་དག་འདུས་ཤིང་འཁོད་ནས་གསོ་སྦྱོང་དང་།
དགག་དབྱེ་དང་། གསོལ་བ་དང་། གསོལ་བ་དང་གཉིས་དང་། གསོལ་བ་དང་བཞིའི་ལས་བྱའོ།
།ལས་མི་མཐུན་པ་དག་བྱས་ན་འགལ་ཚབས་ཅན་དུ་འགྱུར་རོ།
།གསོ་སྦྱོང་གི་གཞི་རྫོགས་སོ།། །། ༄། །དགག་དབྱེའི་གཞི་འཆད་པ། ༈
དགག་དབྱེའི་སྡེ་ཚན། ༆ དགག་དབྱེའི་གཞིའི་བསྡུས་པའི་སྡོམ་ལ། དགག་དབྱེ་
ཆོས་མིན་དགེ་སློང་དང་། །ཟློས་དང་དབྱར་དང་ཕྱི་མའོ། །སྡོམ་ལ།
དགག་དབྱེ་རྗེས་སུ་གནང་བ་དང་། །དགག་དབྱེ་བྱེད་པ་བསྒོ་བ་དང་།
།ཆོས་དང་ཆོས་མིན་གཏོགས་པ་དང་། །ནད་པ་གསོ་བའི་ལས་རྣམས་སོ།
།སངས་རྒྱས་བཅོམ་ལྡན་འདས་མཉན་ཡོད་ན་
རྒྱལ་བུ་རྒྱལ་བྱེད་ཀྱི་ཚལ་མགོན་མེད་ཟས་སྦྱིན་གྱི་ཀུན་དགའ་ར་བར་དབྱར་བཞུགས་པར་དམ་བཞེས་སོ།
།དེའི་ཚེ་དགེ་སློང་རབ་ཏུ་མང་པོ་དག་ལྗོངས་ཤིག་ཏུ་ཁྲིམས་སུ་བཅའ་བ་འདི་ལྟ་བུ་དག་བྱས་ནས་དབྱར་གནས་པར་དམ་བཅས་ཏེ།
ཚེ་དང་ལྡན་པ་དག་བདག་ཅག་གི་ནང་ ༄། །དང་པོ།
སྔོན་གྱི་སྐུའི་སྐྱེད་རབས་ལ་བརྩམས་ཏེ་དམ་པའི་ཡོན་ཏན་ཅི་རིགས་པ་བསྙད་པའི་སྡེ་ཚན།
༄༅། །དཔལ་ལྡན་བླ་མའི་རྣམ་ཐར་འཕྲིན་ལས་རྒྱ་མཚོ་
རྣམ་པར་རྒྱས་པ་ཞེས་བྱ་བ་བཞུགས། ༄༅། །ༀ་སྭཱ་སྟི་པྲ་ཛ་བྷྱཿ
དཔལ་ལྡན་བླ་མ་དམ་པ་མགོན་པོ་སྟག་ལུང་བ་ཆེན་པོ་དོན་
གྱི་སླད་དུ་མཚན་ནས་སྨོས་ཏེ་རྗེ་བཙུན་ཀུན་དགའ་བཀྲ་ཤིས་རྒྱལ་མཚན་དཔལ་བཟང་པོའི་
ཞབས་ཀྱི་རྣམ་པར་ཐར་པ་འཕྲིན་ལས་རྒྱ་མཚོ་རྣམ་པར་རྒྱས་པ་ཞེས་བྱ་བ།
བླ་མ་མཆོག་གི་ ཞབས་ལ་གུས་པས་ཕྱག་འཚལ་ལོ།
།དུས་ཐམས་ཅད་དུ་བརྩེ་བ་ཆེན་པོས་རྗེས་སུ་བཟུང་དུ་ གསོལ།
དཔལ་ལྡན་མཆོག་ཏུ་རྟག་བརྟན་གཞོམ་མེད་སྐུ། །བརྗོད་བྲལ་སྒྲ་དབྱངས་ཀུན་ལ་
འཇུག་འགྱུར་གསུང་། །ལྷུན་འགྲུབ་བདེ་སྟོང་ཇི་སྙེད་གཟིགས་པའི་ཐུགས།
།འཕྲིན་ལས་ རྒྱུན་མི་འཆད་ཁྱོད་ཕྱག་གི་གནས། །རབ་བརྟན་བྱང་ཆུབ་སེམས་ཀྱི་གཞིར།
།རྨད་བྱུང་ཕན་ བདེའི་ལོ་ཏོག་དག །མཐའ་ཡས་བསྐལ་བར་མི་ཟད་པ།
།བསྐྲུན་མཛད་བླ་མ་མཆོག་འདི་ རྒྱལ། །ཨེ་ཡིག་ཀུན་ལྡན་སྟོང་པའི་ཁྲིར།
།ཝཾ་ཡིག་བདེ་ཆེན་རང་རྒྱལ་པོ། །ཟུང་འཇུག་ལྷན་ སྐྱེས་རང་བཞིན་དངོས།
།བླ་མ་རྡོ་རྗེ་ཅན་ཞབས་འདུད། །ཐོག་མེད་རྫུ་འཕྲུལ་བསྒྲོད་པ་ཅན། །
མཚན་དཔེའི་གཟི་བྱིན་དཔག་ཡས་ཀྱི། །འཕྲིན་ལས་ཀུན་ཁྱབ་གར་ཡང་སོན། །སྒྱུ་འཕྲུལ་
སྐུ་ལ་ཕྱག་འཚལ་བསྟོད། །འགགས་མེད་གཞོམ་དུ་མེད་པའི་ཚུལ།
།བརྗོད་བྲལ་སྒྲ་དབྱངས་ ཐམས་ཅད་པ། །སེམས་ཅན་བསམ་པ་ཇི་བཞིན་འཇུག
།འགྲོ་མགོན་གསུང་ལ་ཕྱག་བགྱིའོ། ། རང་བཞིན་དག་པ་རྣམ་མི་རྟོག
།ལྷུན་འགྲུབ་འབད་རྩོལ་བྲལ་བའི་དབྱིངས། །ཇི་ལྟ་ཇི་ སྙེད་ཇི་བཞིན་གཟིགས།
།རྗེ་བཙུན་ཐུགས་ལ་ཕྱག་འཚལ་ལོ། །གང་ཞིག་འགྲོ་ཀུན༷་དག༷འ་
བའི་བཀྲ༷་ཤིས༷་ཆོས་སྒོ་རབ་གསལ་ཐར་པའི་རྒྱལ༷་མཚ༷ན་དཔ༷ལ་ལྡན་སྙིང་རྗེ་བཟ༷ང་པོ༷་
</p>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tibetan Font Display</title>
<style>
@import url("https://fonts.googleapis.com/css2?family=Noto+Serif+Tibetan:[email protected]&display=swap");
body {
margin: 0;
padding: 40px;
background-color: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: system-ui, -apple-system, sans-serif;
}
.container {
background: white;
padding: 60px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
max-width: 800px;
}
.noto-serif-tibetan-mono {
font-family: "Noto Serif Tibetan", serif;
font-size: 38px;
line-height: 1.8;
color: #333;
text-align: left;
}
</style>
</head>
<body>
<div class="container">
<p class="sample noto-serif-tibetan-mono">
ཀ་ཁ་ག་ང་ཅ་ཆ་ཇ་<br />ཉ་ཏ་ཐ་ད་ན་པ་ཕ་<br />བ་མ་ཙ་ཚ་ཛ་ཝ་ཞ་<br />ཟ་འ་ཡ་ར་ལ་ཤ་ས་ཧ་<br />༈འགྲོ་བ་མིའི་ཁྱིམ་ཚང་ཁག་གི་ནང་མི་ཡོངས་ལ་རང་བཞིན་ཉིད་ནས་ཡོད་པའི་ཆེ་མཐོངས་དང་འདྲ་མཉམ།
སུས་ཀྱང་འཕྲོག་ཏུ་མི་རུང་བའི་ཐོབ་ཐང་བཅས་ཀྱི་གནད་དོན་རྟོགས པར་བྱེད་པ་ནི།
འཛམ་གླིང་ནང་གི་རང་དབང་དང༌། དྲང་བདེན། ཞི་བདེ་བཅས་ཀྱི་རྣང་གཞི་ལྟེ་བ་ཡིན།
དུང་དང་འོ་མར་འགྲན་པའི་ལྷག་བསམ་མཐུ། །དམན་ཡང་དཀར་པོའི་བྱས་འབྲས་ཅུང་ཟད་ཅིག
།བློ་དང་འདུན་པ་བཟང་བའི་རང་རིགས་ཀུན།
།རྒྱལ་ཁའི་འཕྲིན་བཟང་ལས་དོན་འགྲུབ་ཕྱིར་འབད།། ༄༅། སྤྱིར། བརྙན།
དེར་སྒྲུབས་པའི་གནས་སུ༔ བརྩམས་པའི་རྨྱུགས་དང་།
སྤྲྱོད་པའི་གྲྭས་ཀྱི་སྐུའི་འོད་ནི། བརྩམས་པའི་མཐའ་ཡས་ཀྱི་བསྒྲུབས་ནས།
སྤྱིར་དེ་དག་ལམ་གྱི་སྒྲོན་མེ་དུ་གསལ། ཡིན།
དེ་བཞིན་གཤེགས་པ་ལ་ངན་སེམས་ཀྱིས་ཁྲག་འབྱིན་པ་རྣམས་དང་མ་ཡིན།
མུ་སྟེགས་ཅན་རྣམས་དང་མ་ཡིན། མུ་སྟེགས་ཅན་ཞུགས་པ་རྣམས་དང་མ་ཡིན།
རྐུ་ཐབས་སུ་གནས་པ་རྣམས་དང་མ་ཡིན། ཐ་དད་དུ་གནས་པ་རྣམས་དང་མ་ཡིན་ཏེ། གནས་
པར་མི་བྱ་བ་རྣམས་དང་འདུས་ཤིང་འཁོད་ནས་གསོ་སྦྱོང་དང་། དགག་དབྱེ་དང་།
གསོལ་བ་དང་། གསོལ་བ་དང་གཉིས་དང་། གསོལ་བ་དང་བཞིའི་ལས་མི་བྱའི།
ཐམས་ཅད་ཡོངས་སུ་དག་པ་དང་། ལྟ་བ་མཐུན་པ་དག་འདུས་ཤིང་འཁོད་ནས་གསོ་སྦྱོང་དང་།
དགག་དབྱེ་དང་། གསོལ་བ་དང་། གསོལ་བ་དང་གཉིས་དང་། གསོལ་བ་དང་བཞིའི་ལས་བྱའོ།
།ལས་མི་མཐུན་པ་དག་བྱས་ན་འགལ་ཚབས་ཅན་དུ་འགྱུར་རོ།
།གསོ་སྦྱོང་གི་གཞི་རྫོགས་སོ།། །། ༄། །དགག་དབྱེའི་གཞི་འཆད་པ། ༈
དགག་དབྱེའི་སྡེ་ཚན། ༆ དགག་དབྱེའི་གཞིའི་བསྡུས་པའི་སྡོམ་ལ། དགག་དབྱེ་
ཆོས་མིན་དགེ་སློང་དང་། །ཟློས་དང་དབྱར་དང་ཕྱི་མའོ། །སྡོམ་ལ།
དགག་དབྱེ་རྗེས་སུ་གནང་བ་དང་། །དགག་དབྱེ་བྱེད་པ་བསྒོ་བ་དང་།
།ཆོས་དང་ཆོས་མིན་གཏོགས་པ་དང་། །ནད་པ་གསོ་བའི་ལས་རྣམས་སོ།
།སངས་རྒྱས་བཅོམ་ལྡན་འདས་མཉན་ཡོད་ན་
རྒྱལ་བུ་རྒྱལ་བྱེད་ཀྱི་ཚལ་མགོན་མེད་ཟས་སྦྱིན་གྱི་ཀུན་དགའ་ར་བར་དབྱར་བཞུགས་པར་དམ་བཞེས་སོ།
།དེའི་ཚེ་དགེ་སློང་རབ་ཏུ་མང་པོ་དག་ལྗོངས་ཤིག་ཏུ་ཁྲིམས་སུ་བཅའ་བ་འདི་ལྟ་བུ་དག་བྱས་ནས་དབྱར་གནས་པར་དམ་བཅས་ཏེ།
ཚེ་དང་ལྡན་པ་དག་བདག་ཅག་གི་ནང་ ༄། །དང་པོ།
སྔོན་གྱི་སྐུའི་སྐྱེད་རབས་ལ་བརྩམས་ཏེ་དམ་པའི་ཡོན་ཏན་ཅི་རིགས་པ་བསྙད་པའི་སྡེ་ཚན།
༄༅། །དཔལ་ལྡན་བླ་མའི་རྣམ་ཐར་འཕྲིན་ལས་རྒྱ་མཚོ་
རྣམ་པར་རྒྱས་པ་ཞེས་བྱ་བ་བཞུགས། ༄༅། །ༀ་སྭཱ་སྟི་པྲ་ཛ་བྷྱཿ
དཔལ་ལྡན་བླ་མ་དམ་པ་མགོན་པོ་སྟག་ལུང་བ་ཆེན་པོ་དོན་
གྱི་སླད་དུ་མཚན་ནས་སྨོས་ཏེ་རྗེ་བཙུན་ཀུན་དགའ་བཀྲ་ཤིས་རྒྱལ་མཚན་དཔལ་བཟང་པོའི་
ཞབས་ཀྱི་རྣམ་པར་ཐར་པ་འཕྲིན་ལས་རྒྱ་མཚོ་རྣམ་པར་རྒྱས་པ་ཞེས་བྱ་བ།
བླ་མ་མཆོག་གི་ ཞབས་ལ་གུས་པས་ཕྱག་འཚལ་ལོ།
།དུས་ཐམས་ཅད་དུ་བརྩེ་བ་ཆེན་པོས་རྗེས་སུ་བཟུང་དུ་ གསོལ།
དཔལ་ལྡན་མཆོག་ཏུ་རྟག་བརྟན་གཞོམ་མེད་སྐུ། །བརྗོད་བྲལ་སྒྲ་དབྱངས་ཀུན་ལ་
འཇུག་འགྱུར་གསུང་། །ལྷུན་འགྲུབ་བདེ་སྟོང་ཇི་སྙེད་གཟིགས་པའི་ཐུགས།
།འཕྲིན་ལས་ རྒྱུན་མི་འཆད་ཁྱོད་ཕྱག་གི་གནས། །རབ་བརྟན་བྱང་ཆུབ་སེམས་ཀྱི་གཞིར།
།རྨད་བྱུང་ཕན་ བདེའི་ལོ་ཏོག་དག །མཐའ་ཡས་བསྐལ་བར་མི་ཟད་པ།
།བསྐྲུན་མཛད་བླ་མ་མཆོག་འདི་ རྒྱལ། །ཨེ་ཡིག་ཀུན་ལྡན་སྟོང་པའི་ཁྲིར།
།ཝཾ་ཡིག་བདེ་ཆེན་རང་རྒྱལ་པོ། །ཟུང་འཇུག་ལྷན་ སྐྱེས་རང་བཞིན་དངོས།
།བླ་མ་རྡོ་རྗེ་ཅན་ཞབས་འདུད། །ཐོག་མེད་རྫུ་འཕྲུལ་བསྒྲོད་པ་ཅན། །
མཚན་དཔེའི་གཟི་བྱིན་དཔག་ཡས་ཀྱི། །འཕྲིན་ལས་ཀུན་ཁྱབ་གར་ཡང་སོན། །སྒྱུ་འཕྲུལ་
སྐུ་ལ་ཕྱག་འཚལ་བསྟོད། །འགགས་མེད་གཞོམ་དུ་མེད་པའི་ཚུལ།
།བརྗོད་བྲལ་སྒྲ་དབྱངས་ ཐམས་ཅད་པ། །སེམས་ཅན་བསམ་པ་ཇི་བཞིན་འཇུག
།འགྲོ་མགོན་གསུང་ལ་ཕྱག་བགྱིའོ། ། རང་བཞིན་དག་པ་རྣམ་མི་རྟོག
།ལྷུན་འགྲུབ་འབད་རྩོལ་བྲལ་བའི་དབྱིངས། །ཇི་ལྟ་ཇི་ སྙེད་ཇི་བཞིན་གཟིགས།
།རྗེ་བཙུན་ཐུགས་ལ་ཕྱག་འཚལ་ལོ། །གང་ཞིག་འགྲོ་ཀུན༷་དག༷འ་
བའི་བཀྲ༷་ཤིས༷་ཆོས་སྒོ་རབ་གསལ་ཐར་པའི་རྒྱལ༷་མཚ༷ན་དཔ༷ལ་ལྡན་སྙིང་རྗེ་བཟ༷ང་པོ༷་
</p>
</div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment