def asFeaAST(self, inFeature=False): if self.name and not inFeature: f = feaast.LookupBlock(name=self.name) elif self.name: f = feaast.LookupBlock(name=self.name) else: f = feaast.Block() arranged = arrange(self) if arranged and inFeature: f = feaast.Block() for a in arranged: f.statements.append(asFeaAST(a, inFeature)) return f if hasattr(self, "flags"): flags = feaast.LookupFlagStatement(self.flags) if self.flags & 0x10 and hasattr(self, "markFilteringSetAsClass"): # XXX # We only need the name, not the contents mfs = feaast.GlyphClassDefinition(self.markFilteringSetAsClass, feaast.GlyphClass([])) flags.markFilteringSet = feaast.GlyphClassName(mfs) if self.flags & 0xFF00 and hasattr(self, "markAttachmentSetAsClass"): # XXX mfs = feaast.GlyphClassDefinition(self.markAttachmentSetAsClass, feaast.GlyphClass([])) flags.markAttachment = feaast.GlyphClassName(mfs) f.statements.append(flags) for x in self.comments: f.statements.append(feaast.Comment(x)) f.statements.append(feaast.Comment(";")) lastaddress = self.address if lastaddress: f.statements.append( feaast.Comment("# Original source: %s " % (" ".join([str(x) for x in lastaddress])))) for x in self.rules: if x.address and x.address != lastaddress: f.statements.append( feaast.Comment("# Original source: %s " % x.address)) lastaddress = x.address f.statements.append(x.asFeaAST()) return f
def asFeaAST(self): lut = lookup_type(self) if not lut: return feaast.Comment("") if lut == 1: # GSUB 1 Single Substitution return feaast.SingleSubstStatement( [glyphref(x) for x in self.input], [glyphref(x) for x in self.replacement], [glyphref(x) for x in self.precontext], [glyphref(x) for x in self.postcontext], False, ) elif lut == 2: # GSUB 2 Multiple Substitution # Paired rules need to become a set of statements if is_paired(self): return paired_mult(self) return feaast.MultipleSubstStatement( [glyphref(x) for x in self.precontext], glyphref(self.input[0]), [glyphref(x) for x in self.postcontext], [glyphref(x) for x in self.replacement], ) elif lut == 3: # GSUB 3 Alternate Substitution return feaast.AlternateSubstStatement( [glyphref(x) for x in self.precontext], glyphref(self.input[0]), [glyphref(x) for x in self.postcontext], feaast.GlyphClass( [feaast.GlyphName(x) for x in self.replacement[0]]), ) elif lut == 4: # GSUB 4 Ligature Substitution # Paired rules need to become a set of statements if is_paired(self): return paired_ligature(self) return feaast.LigatureSubstStatement( [glyphref(x) for x in self.precontext], [glyphref(x) for x in self.input], [glyphref(x) for x in self.postcontext], glyphref(self.replacement[0]), False, ) elif lut in [5, 6, 7 ]: # GSUB 5, 6, 7 Different types of contextual substitutions raise NotImplementedError("Use the Chain verb for this") elif lut == 8: # GSUB 8 Reverse Chaining Single Substitution return feaast.ReverseChainSingleSubstStatement( [glyphref(x) for x in self.precontext], [glyphref(x) for x in self.postcontext], [glyphref(x) for x in self.input], [glyphref(self.replacement[0])], ) elif lut >= 9: raise NotImplementedError( "Invalid GSUB lookup type requested: {}".format(lut)) raise ValueError("LookupType must be a single positive integer")
def asFeaAST(self): if self.name: f = feaast.LookupBlock(name=self.name) else: f = feaast.Block() if hasattr(self, "flags"): flags = feaast.LookupFlagStatement(self.flags) if self.flags & 0x10 and hasattr(self, "markFilteringSetAsClass"): # XXX # We only need the name, not the contents mfs = feaast.GlyphClassDefinition(self.markFilteringSetAsClass, feaast.GlyphClass([])) flags.markFilteringSet = feaast.GlyphClassName(mfs) if self.flags & 0xFF00 and hasattr(self, "markAttachmentSetAsClass"): # XXX mfs = feaast.GlyphClassDefinition(self.markAttachmentSetAsClass, feaast.GlyphClass([])) flags.markAttachment = feaast.GlyphClassName(mfs) f.statements.append(flags) for x in self.comments: f.statements.append(feaast.Comment(x)) f.statements.append(feaast.Comment(";")) lastaddress = self.address if lastaddress: f.statements.append( feaast.Comment("# Original source: %s " % (" ".join([str(x) for x in lastaddress])))) for x in self.rules: if x.address and x.address != lastaddress: f.statements.append( feaast.Comment("# Original source: %s " % x.address)) lastaddress = x.address if hasattr(x, "note"): f.statements.append( feaast.Comment("\n".join( [f"# {n}" for n in x.note.split("\n")]))) f.statements.append(x.asFeaAST()) return f
def _build_end_locations(self): # The statements in the ast only have their start location, but we also # need the end location to find the text in between. # FIXME: (jany) maybe feaLib could provide that? # Add a fake statement at the end, it's the only one that won't get # a proper end_location, but its presence will help compute the # end_location of the real last statement(s). self._lines.append('#') # Line corresponding to the fake statement fake_location = (None, len(self._lines), 1) self._doc.statements.append(ast.Comment(text="Sentinel", location=fake_location)) self._build_end_locations_rec(self._doc) # Remove the fake last statement self._lines.pop() self._doc.statements.pop()
def asFeaAST(self): """Returns this font's features as a feaLib AST object, for later translation to AFDKO code.""" from fontFeatures import Routine, Chaining ff = feaast.FeatureFile() add_language_system_statements(self, ff) add_gdef(self, ff) newRoutines = [self.routines[i] for i in reorderAndResolve(self)] # Preamble for k in newRoutines: assert isinstance(k, Routine) if not k.name: k.name = self.gensym("Routine_") pre = k.feaPreamble(self) if k.rules: ff.statements.extend(pre) for k, v in self.namedClasses.items(): asclass = _to_inline_class(v) ff.statements.append(feaast.GlyphClassDefinition(k, asclass)) ff.statements.append(feaast.Comment("")) for k in newRoutines: if k.rules and k.usecount != 1: ff.statements.append(k.asFeaAST()) expandedLanguages = [] for s, ls in self.scripts_and_languages.items(): for l in ls: expandedLanguages.append((s, l)) for k, v in self.features.items(): f = feaast.FeatureBlock(k) for n in v: f.statements.append(n.asFeaAST(allLanguages=expandedLanguages)) ff.statements.append(f) return ff
def _lookupDefinition(self, lookup): mark_attachement = None mark_filtering = None flags = 0 if lookup.direction == "RTL": flags |= 1 if not lookup.process_base: flags |= 2 # FIXME: Does VOLT support this? # if not lookup.process_ligatures: # flags |= 4 if not lookup.process_marks: flags |= 8 elif isinstance(lookup.process_marks, str): mark_attachement = self._groupName(lookup.process_marks) elif lookup.mark_glyph_set is not None: mark_filtering = self._groupName(lookup.mark_glyph_set) lookupflags = None if flags or mark_attachement is not None or mark_filtering is not None: lookupflags = ast.LookupFlagStatement(flags, mark_attachement, mark_filtering) if "\\" in lookup.name: # Merge sub lookups as subtables (lookups named “base\sub”), # makeotf/feaLib will issue a warning and ignore the subtable # statement if it is not a pairpos lookup, though. name = lookup.name.split("\\")[0] if name.lower() not in self._lookups: fealookup = ast.LookupBlock(self._lookupName(name)) if lookupflags is not None: fealookup.statements.append(lookupflags) fealookup.statements.append(ast.Comment("# " + lookup.name)) else: fealookup = self._lookups[name.lower()] fealookup.statements.append(ast.SubtableStatement()) fealookup.statements.append(ast.Comment("# " + lookup.name)) self._lookups[name.lower()] = fealookup else: fealookup = ast.LookupBlock(self._lookupName(lookup.name)) if lookupflags is not None: fealookup.statements.append(lookupflags) self._lookups[lookup.name.lower()] = fealookup if lookup.comments is not None: fealookup.statements.append(ast.Comment("# " + lookup.comments)) contexts = [] if lookup.context: for context in lookup.context: prefix = self._context(context.left) suffix = self._context(context.right) ignore = context.ex_or_in == "EXCEPT_CONTEXT" contexts.append([prefix, suffix, ignore, False]) # It seems that VOLT will create contextual substitution using # only the input if there is no other contexts in this lookup. if ignore and len(lookup.context) == 1: contexts.append([[], [], False, True]) else: contexts.append([[], [], False, False]) targetlookup = None for prefix, suffix, ignore, chain in contexts: if lookup.sub is not None: self._gsubLookup(lookup, prefix, suffix, ignore, chain, fealookup) if lookup.pos is not None: if self._settings.get("COMPILER_USEEXTENSIONLOOKUPS"): fealookup.use_extension = True if prefix or suffix or chain or ignore: if not ignore and targetlookup is None: targetname = self._lookupName(lookup.name + " target") targetlookup = ast.LookupBlock(targetname) fealookup.targets = getattr(fealookup, "targets", []) fealookup.targets.append(targetlookup) self._gposLookup(lookup, targetlookup) self._gposContextLookup(lookup, prefix, suffix, ignore, fealookup, targetlookup) else: self._gposLookup(lookup, fealookup)
def _buildFeatureFile(self, tables): doc = ast.FeatureFile() statements = doc.statements if self._glyphclasses: statements.append(ast.Comment("# Glyph classes")) statements.extend(self._glyphclasses.values()) if self._markclasses: statements.append(ast.Comment("\n# Mark classes")) statements.extend(c[1] for c in sorted(self._markclasses.items())) if self._lookups: statements.append(ast.Comment("\n# Lookups")) for lookup in self._lookups.values(): statements.extend(getattr(lookup, "targets", [])) statements.append(lookup) # Prune features features = self._features.copy() for ftag in features: scripts = features[ftag] for stag in scripts: langs = scripts[stag] for ltag in langs: langs[ltag] = [ l for l in langs[ltag] if l.lower() in self._lookups ] scripts[stag] = {t: l for t, l in langs.items() if l} features[ftag] = {t: s for t, s in scripts.items() if s} features = {t: f for t, f in features.items() if f} if features: statements.append(ast.Comment("# Features")) for ftag, scripts in features.items(): feature = ast.FeatureBlock(ftag) stags = sorted(scripts, key=lambda k: 0 if k == "DFLT" else 1) for stag in stags: feature.statements.append(ast.ScriptStatement(stag)) ltags = sorted(scripts[stag], key=lambda k: 0 if k == "dflt" else 1) for ltag in ltags: include_default = True if ltag == "dflt" else False feature.statements.append( ast.LanguageStatement( ltag, include_default=include_default)) for name in scripts[stag][ltag]: lookup = self._lookups[name.lower()] lookupref = ast.LookupReferenceStatement(lookup) feature.statements.append(lookupref) statements.append(feature) if self._gdef and "GDEF" in tables: classes = [] for name in ("BASE", "MARK", "LIGATURE", "COMPONENT"): if name in self._gdef: classname = "GDEF_" + name.lower() glyphclass = ast.GlyphClassDefinition( classname, self._gdef[name]) statements.append(glyphclass) classes.append(ast.GlyphClassName(glyphclass)) else: classes.append(None) gdef = ast.TableBlock("GDEF") gdef.statements.append(ast.GlyphClassDefStatement(*classes)) statements.append(gdef) return doc
def asFeaAST(self, do_gdef=True): """Returns this font's features as a feaLib AST object, for later translation to AFDKO code.""" from fontFeatures import Routine, Chaining ff = feaast.FeatureFile() add_language_system_statements(self, ff) if do_gdef: add_gdef(self, ff) # In OpenType, we need to rearrange the routines such that all rules for # a given languagesystem, lookup type, and lookup flags, appear in the # same lookup. Also, lookups with the same languagesystem need to appear next # to one another, because FEA syntax is stupid. # Now arrange them by type/etc. for k, v in self.features.items(): for reference in v: routine = reference.routine # If a rule has >1 language it must first be split newrules = [] for r in routine.rules: if len(r.languages or []) > 1: for language in r.languages: newrule = copy.copy(r) newrule.languages = [language] newrules.append(newrule) else: newrules.append(r) routine.rules = newrules partitioned = self.partitionRoutine( routine, lambda rule: tuple([ tuple(rule.languages or []), type(rule), lookup_type(rule) ])) if routine.name and partitioned and len(partitioned) > 1: for p in partitioned: rule = p.rules[0] language = (rule.languages or [("DFLT", "dflt")])[0] p.name = p.name + "%s_%s_%s_%i" % ( language[0].strip(), language[1].strip(), type(rule).__name__, lookup_type(rule)) for r in self.routines: r.usecount = 0 # Bubble up flags and languages if r.rules and not r.flags: r.flags = r.rules[0].flags if r.rules and not r.languages: r.languages = r.rules[0].languages for k, v in self.features.items(): # Similarly split routines with multiple languages references = [] for reference in v: routine = reference.routine if len(routine.languages or []) > 1: splitroutines = [] for language in routine.languages: # This is wrong. It should really be a new reference to the # same routine. But this will do for now. newreference = copy.copy(reference) newreference.languages = [language] references.append(newreference) else: references.append(reference) reference.languages = routine.languages self.features[k] = references # Order the arranged routines by language # new_references = list(sorted(v, key=lambda x: tuple(x.routine.languages or []))) # Next, we'll ensure that all chaining lookups are resolved and in the right order newRoutines = [self.routines[i] for i in reorderAndResolve(self)] # Preamble for k in newRoutines: assert isinstance(k, Routine) if not k.name and k.usecount != 1: k.name = self.gensym("Routine_") pre = k.feaPreamble(self) if k.rules: ff.statements.extend(pre) for k, v in self.namedClasses.items(): asclass = _to_inline_class(v) ff.statements.append(feaast.GlyphClassDefinition(k, asclass)) ff.statements.append(feaast.Comment("")) for k in newRoutines: if k.rules: ff.statements.append(k.asFeaAST()) for k, v in self.features.items(): for routine in v: # Putting each routine in its own feature saves problems... lang = routine.languages f = feaast.FeatureBlock(k) if lang: f.statements.append(feaast.ScriptStatement(lang[0][0])) f.statements.append( feaast.LanguageStatement("%4s" % lang[0][1])) f.statements.append(routine.asFeaAST(expand=k == "aalt")) ff.statements.append(f) return ff