# Generate features for ligatures # # Adapted from https://github.com/tonsky/FiraCode/blob/master/gen_calt.clj from textwrap import dedent from collections import defaultdict import tempfile def update_features(font): """Find ligatures in the font and generate features for them.""" # [ ["dash" "greater" "greater"] ... ] ligas = [name[:-len('.liga')].split('_') for name in font if name.endswith('.liga') and font[name].isWorthOutputting()] rules = '\n\n'.join(rule(liga) for liga in sorted(ligas, key=lambda l: -len(l))) fea_code = dedent('''\ languagesystem DFLT dflt; languagesystem latn dflt; languagesystem grek dflt; languagesystem cyrl dflt; feature calt {{ {} }} calt; ''').format(indent(rules, ' ')) # print(fea_code) # DEBUG # Add the dummy "LIG" glyph lig = font.createChar(-1, 'LIG') lig.width = font['space'].width with tempfile.NamedTemporaryFile(suffix='.fea') as f: f.write(fea_code) f.seek(0) font.mergeFeature(f.name) def rule(liga): """ [f f i] => { [LIG LIG i] f_f_i.liga [LIG f i] LIG [ f f i] LIG } """ rules = [] # standard ignores: # ignore sub {0} {0}' {1}; # ignore sub {0}' {1} {1}; if tuple(liga) not in skip_ignores: rules.extend( [ ignore(prefix=liga[:1], head=liga[0], suffix=liga[1:]), ignore(head=liga[0], suffix=(liga[1:] + [liga[-1]])), ] ) # careful with repeats: # #133 ->->->->, /**/**/**/, etc. if len(liga) > 2 and liga[0] == liga[-1]: rules.append(ignore([liga[-2]], liga[0], liga[1:])) rules.append(ignore(head=liga[0], suffix=(liga[1:] + [liga[1]]))) # Don't cut into `prefix` to complete a ligature. # i.e. regex `(?=`> is not `(?`=>. rules.extend( [ ignore(prefix[:-n], liga[0], liga[1:]) for prefix in ignore_prefixes for n in range(1, len(liga)) if prefix[-n:] == liga[:n] ] ) # hardcoded ignores, i.e. `<||>` rules.extend(ignores[tuple(liga)]) name = "_".join(liga) # substitution logic # sub {0}' {1} by LIG; # sub LIG {1}' by {0}_{1}.liga; for i in range(len(liga)): init = _join(["LIG" for lig in liga[:i]]) tail = _join(liga[i + 1 :]) replace = "LIG" if (i + 1 < len(liga)) else (name + ".liga") rules.append("sub{0} {1}'{2} by {3};".format(init, liga[i], tail, replace)) # put it all together lines = ( ["lookup " + name + " {"] + [" " + r for r in rules] + ["}} {0};".format(name)] ) return "\n".join(lines) def _join(items): return (" " + " ".join(items)) if items else "" def ignore(prefix=None, head=None, suffix=None): """ don't substitute `head` if it's surrounded by `prefix` and `suffix` """ assert head pref = _join(prefix) rest = _join(suffix) return "ignore sub{0} {1}'{2};".format(pref, head, rest) ignores = defaultdict( list, { ("slash", "asterisk"): [ "ignore sub slash' asterisk slash;", "ignore sub asterisk slash' asterisk;", ], ("asterisk", "slash"): [ "ignore sub slash asterisk' slash;", "ignore sub asterisk' slash asterisk;", ], # ("asterisk", "asterisk"): [ # "ignore sub slash asterisk' asterisk;", # "ignore sub asterisk' asterisk slash;", # ], # ("asterisk", "asterisk", "asterisk"): [ # "ignore sub slash asterisk' asterisk asterisk;", # "ignore sub asterisk' asterisk asterisk slash;", # ], # <||> ("less", "bar", "bar"): ["ignore sub less' bar bar greater;"], ("bar", "bar", "greater"): ["ignore sub less bar' bar greater;"], # # :>= # ("colon", "greater"): ["ignore sub colon' greater equal;"], # # {|} # ("braceleft", "bar"): ["ignore sub braceleft' bar braceright;"], # ("bar", "braceright"): ["ignore sub braceleft bar' braceright;"], # # [|] # ("bracketleft", "bar"): ["ignore sub bracketleft' bar bracketright;"], # ("bar", "bracketright"): ["ignore sub bracketleft bar' bracketright;"], # # <*>>> <+>>> <$>>> # ("greater", "greater", "greater"): [ # "ignore sub [asterisk plus dollar] greater' greater greater;" # ], # # <<<*> <<<+> <<<$> # ("less", "less", "less"): ["ignore sub less' less less [asterisk plus dollar];"], # # =:= # ("colon", "equal"): ["ignore sub equal colon' equal;"], # =!= ("exclam", "equal"): ["ignore sub equal exclam' equal;"], # =!== ("exclam", "equal", "equal"): ["ignore sub equal exclam' equal equal;"], # =<= <=< <=> <=| <=: <=! <=/ ("less", "equal"): [ "ignore sub equal less' equal;", "ignore sub less' equal [less greater bar colon exclam slash];", ], # >=< # =>= >=> >=< >=| >=: >=! >=/ ("greater", "equal"): [ "ignore sub equal greater' equal;", "ignore sub greater' equal [less greater bar colon exclam slash];", ], # <*>> <+>> <$>> # >>->> >>=>> ("greater", "greater"): [ # "ignore sub [asterisk plus dollar] greater' greater;", # "ignore sub [hyphen equal] greater' greater;", # "ignore sub greater' greater [hyphen equal];", ], # <<*> <<+> <<$> # <<-<< <<=<< ("less", "less"): [ # "ignore sub less' less [asterisk plus dollar];", # "ignore sub [hyphen equal] less' less;", # "ignore sub less' less [hyphen equal];", ], # ||-|| ||=|| ("bar", "bar"): [ "ignore sub [hyphen equal] bar' bar;", "ignore sub bar' bar [hyphen equal];", ], # # <--> >--< |--| # ("hyphen", "hyphen"): [ # "ignore sub [less greater bar] hyphen' hyphen;", # "ignore sub hyphen' hyphen [less greater bar];", # ], # # <---> >---< |---| # ("hyphen", "hyphen", "hyphen"): # "ignore sub [less greater bar] hyphen' hyphen hyphen;", # "ignore sub hyphen' hyphen hyphen [less greater bar];", # ], ("equal", "equal"): [ # == # "ignore sub bracketleft equal' equal;", # [== # "ignore sub equal' equal bracketright;",# ==] "ignore sub equal [colon exclam] equal' equal;", # =:== =!== # "ignore sub [less greater bar slash] equal' equal;", # <== >== |== /== # "ignore sub equal' equal [less greater bar slash];", # ==< ==> ==| ==/ "ignore sub equal' equal [colon exclam] equal;", # ==:= ==!= ], # [===[ ]===] # [=== ===] # <===> >===< |===| /===/ =:=== =!=== ===:= ===!= ("equal", "equal", "equal"): [ # "ignore sub bracketleft equal' equal equal;", # "ignore sub equal' equal equal bracketright;", "ignore sub equal [colon exclam] equal' equal equal;", "ignore sub [less greater bar slash] equal' equal equal;", # "ignore sub equal' equal equal [less greater bar slash];", "ignore sub equal' equal equal [colon exclam] equal;", ], # #118 https:// ("slash", "slash"): ["ignore sub colon slash' slash;"], }, ) ignore_prefixes = [ ["parenleft", "question", "colon"], # Regexp lookahead/lookbehind ["parenleft", "question", "equal"], ["parenleft", "question", "less", "equal"], ["parenleft", "question", "exclam"], ["parenleft", "question", "less", "exclam"], # PHP > <<+>> <<$>> # ("less", "asterisk", "greater"), # ("less", "plus", "greater"), # ("less", "dollar", "greater"), } def indent(text, prefix): return '\n'.join(prefix + line for line in text.split('\n'))