diff options
Diffstat (limited to '')
-rw-r--r-- | src/silfont/feax_ast.py | 459 |
1 files changed, 459 insertions, 0 deletions
diff --git a/src/silfont/feax_ast.py b/src/silfont/feax_ast.py new file mode 100644 index 0000000..13f76fb --- /dev/null +++ b/src/silfont/feax_ast.py @@ -0,0 +1,459 @@ +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 "<anchor NULL>", 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 "<anchor NULL>"), + (exit.anchor.asFea() if exit else "<anchor NULL>")) + 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 = ["<anchor NULL>"] + 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) + |