import ast as pyast from fontTools.feaLib import ast from fontTools.feaLib.ast import asFea from fontTools.feaLib.error import FeatureLibError import re, math def asFea(g): if hasattr(g, 'asClassFea'): return g.asClassFea() elif hasattr(g, 'asFea'): return g.asFea() elif isinstance(g, tuple) and len(g) == 2: return asFea(g[0]) + "-" + asFea(g[1]) # a range elif g.lower() in ast.fea_keywords: return "\\" + g else: return g ast.asFea = asFea SHIFT = ast.SHIFT def asLiteralFea(self, indent=""): Element.mode = 'literal' return self.asFea(indent=indent) Element.mode = 'flat' ast.Element.asLiteralFea = asLiteralFea ast.Element.mode = 'flat' class ast_Comment(ast.Comment): def __init__(self, text, location=None): super(ast_Comment, self).__init__(text, location=location) self.pretext = "" self.posttext = "" def asFea(self, indent=""): return self.pretext + self.text + self.posttext class ast_MarkClass(ast.MarkClass): # This is better fixed upstream in parser.parse_glyphclass_ to handle MarkClasses def asClassFea(self, indent=""): return "[" + " ".join(map(asFea, self.glyphs)) + "]" class ast_BaseClass(ast_MarkClass) : def asFea(self, indent="") : return "@" + self.name + " = [" + " ".join(map(asFea, self.glyphs.keys())) + "];" class ast_BaseClassDefinition(ast.MarkClassDefinition): def asFea(self, indent="") : # like base class asFea return ("# " if self.mode != 'literal' else "") + \ "{}baseClass {} {} @{};".format(indent, self.glyphs.asFea(), self.anchor.asFea(), self.markClass.name) class ast_MarkBasePosStatement(ast.MarkBasePosStatement): def asFea(self, indent=""): # handles members added by parse_position_base_ with feax syntax if isinstance(self.base, ast.MarkClassName): # flattens pos @BASECLASS mark @MARKCLASS res = "" if self.mode == 'literal': res += "pos base @{} ".format(self.base.markClass.name) res += " ".join("mark @{}".format(m.name) for m in self.marks) res += ";" else: for bcd in self.base.markClass.definitions: if res != "": res += "\n{}".format(indent) res += "pos base {} {}".format(bcd.glyphs.asFea(), bcd.anchor.asFea()) res += "".join(" mark @{}".format(m.name) for m in self.marks) res += ";" else: # like base class method res = "pos base {}".format(self.base.asFea()) res += "".join(" {} mark @{}".format(a.asFea(), m.name) for a, m in self.marks) res += ";" return res def build(self, builder) : #TODO: do the right thing here (write to ttf?) pass class ast_MarkMarkPosStatement(ast.MarkMarkPosStatement): # super class __init__() for reference # def __init__(self, location, baseMarks, marks): # Statement.__init__(self, location) # self.baseMarks, self.marks = baseMarks, marks def asFea(self, indent=""): # handles members added by parse_position_base_ with feax syntax if isinstance(self.baseMarks, ast.MarkClassName): # flattens pos @MARKCLASS mark @MARKCLASS res = "" if self.mode == 'literal': res += "pos mark @{} ".format(self.base.markClass.name) res += " ".join("mark @{}".format(m.name) for m in self.marks) res += ";" else: for mcd in self.baseMarks.markClass.definitions: if res != "": res += "\n{}".format(indent) res += "pos mark {} {}".format(mcd.glyphs.asFea(), mcd.anchor.asFea()) for m in self.marks: res += " mark @{}".format(m.name) res += ";" else: # like base class method res = "pos mark {}".format(self.baseMarks.asFea()) for a, m in self.marks: res += " {} mark @{}".format(a.asFea() if a else "", m.name) res += ";" return res def build(self, builder): # builder.add_mark_mark_pos(self.location, self.baseMarks.glyphSet(), self.marks) #TODO: do the right thing pass class ast_CursivePosStatement(ast.CursivePosStatement): # super class __init__() for reference # def __init__(self, location, glyphclass, entryAnchor, exitAnchor): # Statement.__init__(self, location) # self.glyphclass = glyphclass # self.entryAnchor, self.exitAnchor = entryAnchor, exitAnchor def asFea(self, indent=""): if isinstance(self.exitAnchor, ast.MarkClass): # pos cursive @BASE1 @BASE2 res = "" if self.mode == 'literal': res += "pos cursive @{} @{};".format(self.glyphclass.name, self.exitAnchor.name) else: allglyphs = set(self.glyphclass.glyphSet()) allglyphs.update(self.exitAnchor.glyphSet()) for g in sorted(allglyphs): entry = self.glyphclass.glyphs.get(g, None) exit = self.exitAnchor.glyphs.get(g, None) if res != "": res += "\n{}".format(indent) res += "pos cursive {} {} {};".format(g, (entry.anchor.asFea() if entry else ""), (exit.anchor.asFea() if exit else "")) else: res = super(ast_CursivePosStatement, self).asFea(indent) return res def build(self, builder) : #TODO: do the right thing here (write to ttf?) pass class ast_MarkLigPosStatement(ast.MarkLigPosStatement): def __init__(self, ligatures, marks, location=None): ast.MarkLigPosStatement.__init__(self, ligatures, marks, location) self.classBased = False for l in marks: if l is not None: for m in l: if m is not None and not isinstance(m[0], ast.Anchor): self.classBased = True break def build(self, builder): builder.add_mark_lig_pos(self.location, self.ligatures.glyphSet(), self.marks) def asFea(self, indent=""): if not self.classBased or self.mode == "literal": return super(ast_MarkLigPosStatement, self).asFea(indent) res = [] for g in self.ligatures.glyphSet(): comps = [] for l in self.marks: onecomp = [] if l is not None and len(l): for a, m in l: if not isinstance(a, ast.Anchor): if g not in a.markClass.glyphs: continue left = a.markClass.glyphs[g].anchor.asFea() else: left = a.asFea() onecomp.append("{} mark @{}".format(left, m.name)) if not len(onecomp): onecomp = [""] comps.append(" ".join(onecomp)) res.append("pos ligature {} ".format(asFea(g)) + ("\n"+indent+SHIFT+"ligComponent ").join(comps)) return (";\n"+indent).join(res) + ";" #similar to ast.MultipleSubstStatement #one-to-many substitution, one glyph class is on LHS, multiple glyph classes may be on RHS # equivalent to generation of one stmt for each glyph in the LHS class # that's matched to corresponding glyphs in the RHS classes #prefix and suffx are for contextual lookups and do not need processing #replacement could contain multiple slots #TODO: below only supports one RHS class? class ast_MultipleSubstStatement(ast.Statement): def __init__(self, prefix, glyph, suffix, replacement, forceChain, location=None): ast.Statement.__init__(self, location) self.prefix, self.glyph, self.suffix = prefix, glyph, suffix self.replacement = replacement self.forceChain = forceChain lenglyphs = len(self.glyph.glyphSet()) for i, r in enumerate(self.replacement) : if len(r.glyphSet()) == lenglyphs: self.multindex = i #first RHS slot with a glyph class break else: if lenglyphs > 1: raise FeatureLibError("No replacement class is of the same length as the matching class", location) else: self.multindex = 0; def build(self, builder): prefix = [p.glyphSet() for p in self.prefix] suffix = [s.glyphSet() for s in self.suffix] glyphs = self.glyph.glyphSet() replacements = self.replacement[self.multindex].glyphSet() lenglyphs = len(glyphs) for i in range(max(lenglyphs, len(replacements))) : builder.add_multiple_subst( self.location, prefix, glyphs[i if lenglyphs > 1 else 0], suffix, self.replacement[0:self.multindex] + [replacements[i]] + self.replacement[self.multindex+1:], self.forceChain) def asFea(self, indent=""): res = "" pres = (" ".join(map(asFea, self.prefix)) + " ") if len(self.prefix) else "" sufs = (" " + " ".join(map(asFea, self.suffix))) if len(self.suffix) else "" mark = "'" if len(self.prefix) or len(self.suffix) or self.forceChain else "" if self.mode == 'literal': res += "sub " + pres + self.glyph.asFea() + mark + sufs + " by " res += " ".join(asFea(g) for g in self.replacement) + ";" return res glyphs = self.glyph.glyphSet() replacements = self.replacement[self.multindex].glyphSet() lenglyphs = len(glyphs) count = max(lenglyphs, len(replacements)) for i in range(count) : res += ("\n" + indent if i > 0 else "") + "sub " + pres res += asFea(glyphs[i if lenglyphs > 1 else 0]) + mark + sufs res += " by " res += " ".join(asFea(g) for g in self.replacement[0:self.multindex] + [replacements[i]] + self.replacement[self.multindex+1:]) res += ";" return res # similar to ast.LigatureSubstStatement # many-to-one substitution, one glyph class is on RHS, multiple glyph classes may be on LHS # equivalent to generation of one stmt for each glyph in the RHS class # that's matched to corresponding glyphs in the LHS classes # it's unclear which LHS class should correspond to the RHS class # prefix and suffx are for contextual lookups and do not need processing # replacement could contain multiple slots #TODO: below only supports one LHS class? class ast_LigatureSubstStatement(ast.Statement): def __init__(self, prefix, glyphs, suffix, replacement, forceChain, location=None): ast.Statement.__init__(self, location) self.prefix, self.glyphs, self.suffix = (prefix, glyphs, suffix) self.replacement, self.forceChain = replacement, forceChain lenreplace = len(self.replacement.glyphSet()) for i, g in enumerate(self.glyphs): if len(g.glyphSet()) == lenreplace: self.multindex = i #first LHS slot with a glyph class break else: if lenreplace > 1: raise FeatureLibError("No class matches replacement class length", location) else: self.multindex = 0 def build(self, builder): prefix = [p.glyphSet() for p in self.prefix] glyphs = [g.glyphSet() for g in self.glyphs] suffix = [s.glyphSet() for s in self.suffix] replacements = self.replacement.glyphSet() lenreplace = len(replacements.glyphSet()) glyphs = self.glyphs[self.multindex].glyphSet() for i in range(max(len(glyphs), len(replacements))): builder.add_ligature_subst( self.location, prefix, self.glyphs[:self.multindex] + glyphs[i] + self.glyphs[self.multindex+1:], suffix, replacements[i if lenreplace > 1 else 0], self.forceChain) def asFea(self, indent=""): res = "" pres = (" ".join(map(asFea, self.prefix)) + " ") if len(self.prefix) else "" sufs = (" " + " ".join(map(asFea, self.suffix))) if len(self.suffix) else "" mark = "'" if len(self.prefix) or len(self.suffix) or self.forceChain else "" if self.mode == 'literal': res += "sub " + pres + " ".join(asFea(g)+mark for g in self.glyphs) + sufs + " by " res += self.replacements.asFea() + ";" return res glyphs = self.glyphs[self.multindex].glyphSet() replacements = self.replacement.glyphSet() lenreplace = len(replacements) count = max(len(glyphs), len(replacements)) for i in range(count) : res += ("\n" + indent if i > 0 else "") + "sub " + pres res += " ".join(asFea(g)+mark for g in self.glyphs[:self.multindex] + [glyphs[i]] + self.glyphs[self.multindex+1:]) res += sufs + " by " res += asFea(replacements[i if lenreplace > 1 else 0]) res += ";" return res class ast_AlternateSubstStatement(ast.Statement): def __init__(self, prefix, glyphs, suffix, replacements, location=None): ast.Statement.__init__(self, location) self.prefix, self.glyphs, self.suffix = (prefix, glyphs, suffix) self.replacements = replacements def build(self, builder): prefix = [p.glyphSet() for p in self.prefix] suffix = [s.glyphSet() for s in self.suffix] l = len(self.glyphs.glyphSet()) for i, glyph in enumerate(self.glyphs.glyphSet()): replacement = self.replacements.glyphSet()[i::l] builder.add_alternate_subst(self.location, prefix, glyph, suffix, replacement) def asFea(self, indent=""): res = "" l = len(self.glyphs.glyphSet()) for i, glyph in enumerate(self.glyphs.glyphSet()): if i > 0: res += "\n" + indent res += "sub " if len(self.prefix) or len(self.suffix): if len(self.prefix): res += " ".join(map(asFea, self.prefix)) + " " res += asFea(glyph) + "'" # even though we really only use 1 if len(self.suffix): res += " " + " ".join(map(asFea, self.suffix)) else: res += asFea(glyph) res += " from " replacements = ast.GlyphClass(glyphs=self.replacements.glyphSet()[i::l], location=self.location) res += asFea(replacements) res += ";" return res class ast_IfBlock(ast.Block): def __init__(self, testfn, name, cond, location=None): ast.Block.__init__(self, location=location) self.testfn = testfn self.name = name def asFea(self, indent=""): if self.mode == 'literal': res = "{}if{}({}) {{".format(indent, name, cond) res += ast.Block.asFea(self, indent=indent) res += indent + "}\n" return res elif self.testfn(): return ast.Block.asFea(self, indent=indent) else: return "" class ast_DoSubStatement(ast.Statement): def __init__(self, varnames, location=None): ast.Statement.__init__(self, location=location) self.names = varnames def items(self, variables): yield ((None, None),) class ast_DoForSubStatement(ast_DoSubStatement): def __init__(self, varname, glyphs, location=None): ast_DoSubStatement.__init__(self, [varname], location=location) self.glyphs = glyphs.glyphSet() def items(self, variables): for g in self.glyphs: yield((self.names[0], g),) def safeeval(exp): # no dunders in attribute names for n in pyast.walk(pyast.parse(exp)): v = getattr(n, 'id', "") # if v in ('_getiter_', '__next__'): # continue if "__" in v: return False return True class ast_DoLetSubStatement(ast_DoSubStatement): def __init__(self, varnames, expression, parser, location=None): ast_DoSubStatement.__init__(self, varnames, location=location) self.parser = parser if not safeeval(expression): expression='"Unsafe Expression"' self.expr = expression def items(self, variables): gbls = dict(self.parser.fns, **variables) try: v = eval(self.expr, gbls) except Exception as e: raise FeatureLibError(str(e) + " in " + self.expr, self.location) if self.names is None: # in an if yield((None, v),) elif len(self.names) == 1: yield((self.names[0], v),) else: yield(zip(self.names, list(v) + [None] * (len(self.names) - len(v)))) class ast_DoForLetSubStatement(ast_DoLetSubStatement): def items(self, variables): gbls = dict(self.parser.fns, **variables) try: v = eval(self.expr, gbls) except Exception as e: raise FeatureLibError(str(e) + " in " + self.expr, self.location) if len(self.names) == 1: for e in v: yield((self.names[0], e),) else: for e in v: yield(zip(self.names, list(e) + [None] * (len(self.names) - len(e)))) class ast_DoIfSubStatement(ast_DoLetSubStatement): def __init__(self, expression, parser, block, location=None): ast_DoLetSubStatement.__init__(self, None, expression, parser, location=None) self.block = block def items(self, variables): (_, v) = list(ast_DoLetSubStatement.items(self, variables))[0][0] yield (None, (v if v else None),) class ast_KernPairsStatement(ast.Statement): def __init__(self, kerninfo, location=None): super(ast_KernPairsStatement, self).__init__(location) self.kerninfo = kerninfo def asFea(self, indent=""): # return ("\n"+indent).join("pos {} {} {};".format(k1, round(v), k2) \ # for k1, x in self.kerninfo.items() for k2, v in x.items()) coverage = set() rules = dict() # first sort into lists by type of rule for k1, x in self.kerninfo.items(): for k2, v in x.items(): # Determine pair kern type, where: # 'gg' = glyph-glyph, 'gc' = glyph-class', 'cg' = class-glyph, 'cc' = class-class ruleType = 'gc'[k1[0]=='@'] + 'gc'[k2[0]=='@'] rules.setdefault(ruleType, list()).append([k1, round(v), k2]) # for glyph-glyph rules, make list of first glyphs: if ruleType == 'gg': coverage.add(k1) # Now assemble lines in order and convert gc rules to gg where possible: res = [] for ruleType in filter(lambda x: x in rules, ('gg', 'gc', 'cg', 'cc')): if ruleType != 'gc': res.extend(['pos {} {} {};'.format(k1, v, k2) for k1,v,k2 in rules[ruleType]]) else: res.extend(['enum pos {} {} {};'.format(k1, v, k2) for k1, v, k2 in rules[ruleType] if k1 not in coverage]) res.extend(['pos {} {} {};'.format(k1, v, k2) for k1, v, k2 in rules[ruleType] if k1 in coverage]) return ("\n"+indent).join(res)