text: #!/usr/bin/python3
  __doc__ = 'Make features.fea file'
  # TODO: add conditional compilation, compare to fea, compile to ttf
  __url__ = 'http://github.com/silnrsi/pysilfont'
  __copyright__ = 'Copyright (c) 2017 SIL International  (http://www.sil.org)'
  __license__ = 'Released under the MIT License (http://opensource.org/licenses/MIT)'
  __author__ = 'Martin Hosken, Alan Ward'
  
  import silfont.ufo as ufo
  from collections import OrderedDict
  from silfont.feax_parser import feaplus_parser
  from xml.etree import ElementTree as et
  import re
  
  from silfont.core import execute
  
  def getbbox(g):
      res = (65536, 65536, -65536, -65536)
      if g['outline'] is None:
          return (0, 0, 0, 0)
      for c in g['outline'].contours:
          for p in c['point']:
              if 'type' in p.attrib:      # any actual point counts
                  x = float(p.get('x', '0'))
                  y = float(p.get('y', '0'))
                  res = (min(x, res[0]), min(y, res[1]), max(x, res[2]), max(y, res[3]))
      return res
  
  class Glyph(object) :
      def __init__(self, name, advance=0, bbox=None) :
          self.name = name
          self.anchors = {}
          self.is_mark = False
          self.advance = int(float(advance))
          self.bbox = bbox or (0, 0, 0, 0)
  
      def add_anchor(self, info) :
          self.anchors[info['name']] = (int(float(info['x'])), int(float(info['y'])))
  
      def decide_if_mark(self) :
          for a in self.anchors.keys() :
              if a.startswith("_") :
                  self.is_mark = True
                  break
  
  def decode_element(e):
      '''Convert plist element into python structures'''
      res = None
      if e.tag == 'string':
          return e.text
      elif e.tag == 'integer':
          return int(e.text)
      elif e.tag== 'real':
          return float(e.text)
      elif e.tag == 'array':
          res = [decode_element(x) for x in e]
      elif e.tag == 'dict':
          res = {}
          for p in zip(e[::2], e[1::2]):
              res[p[0].text] = decode_element(p[1])
      return res
  
  class Font(object) :
      def __init__(self, defines = None):
          self.glyphs = OrderedDict()
          self.classes = OrderedDict()
          self.all_aps = OrderedDict()
          self.fontinfo = {}
          self.kerns = {}
          self.defines = {} if defines is None else defines
  
      def readaps(self, filename, omitaps='', params = None) :
          omittedaps = set(omitaps.replace(',',' ').split())  # allow comma- and/or space-separated list
          if filename.endswith('.ufo') :
              f = ufo.Ufont(filename, params = params)
              self.fontinfo = {}
              for k, v in f.fontinfo._contents.items():
                  self.fontinfo[k] = decode_element(v[1])
              for g in f.deflayer :
                  ufo_g = f.deflayer[g]
                  advb = ufo_g['advance']
                  adv = advb.width if advb is not None else 0
                  bbox = getbbox(ufo_g)
                  glyph = Glyph(g, advance=adv, bbox=bbox)
                  self.glyphs[g] = glyph
                  if 'anchor' in ufo_g._contents :
                      for a in ufo_g._contents['anchor'] :
                          if a.element.attrib['name'] not in omittedaps:
                              glyph.add_anchor(a.element.attrib)
                              self.all_aps.setdefault(a.element.attrib['name'], []).append(glyph)
              if hasattr(f, 'groups'):
                  for k, v in f.groups._contents.items():
                      self.classes[k.lstrip('@')] = decode_element(v[1])
              if hasattr(f, 'kerning'):
                  for k, v in f.kerning._contents.items():
                      key = k.lstrip('@')
                      if key in self.classes:
                          key = "@" + key
                      subelements = decode_element(v[1])
                      kerndict = {}
                      for s, n in subelements.items():
                          skey = s.lstrip('@')
                          if skey in self.classes:
                              skey = "@" + skey
                          kerndict[skey] = n
                      self.kerns[key] = kerndict
          elif filename.endswith('.xml') :
              currGlyph = None
              currPoint = None
              self.fontinfo = {}
              for event, elem in et.iterparse(filename, events=('start', 'end')):
                  if event == 'start':
                      if elem.tag == 'glyph':
                          name = elem.get('PSName', '')
                          if name:
                              currGlyph = Glyph(name)
                              self.glyphs[name] = currGlyph
                          currPoint = None
                      elif elem.tag == 'point':
                          currPoint = {'name' : elem.get('type', '')}
                      elif elem.tag == 'location' and currPoint is not None:
                          currPoint['x'] = int(elem.get('x', 0))
                          currPoint['y'] = int(elem.get('y', 0))
                      elif elem.tag == 'font':
                          n = elem.get('name', '')
                          x = n.split('-')
                          if len(x) == 2:
                              self.fontinfo['familyName'] = x[0]
                              self.fontinfo['openTypeNamePreferredFamilyName'] = x[0]
                              self.fontinfo['styleMapFamilyName'] = x[0]
                              self.fontinfo['styleName'] = x[1]
                              self.fontinfo['openTypeNamePreferredSubfamilyName'] = x[1]
                              self.fontinfo['postscriptFullName'] = "{0} {1}".format(*x)
                          self.fontinfo['postscriptFontName'] = n
                  elif event == 'end':
                      if elem.tag == 'point':
                          if currGlyph and currPoint['name'] not in omittedaps:
                              currGlyph.add_anchor(currPoint)
                              self.all_aps.setdefault(currPoint['name'], []).append(currGlyph)
                          currPoint = None
                      elif elem.tag == 'glyph':
                          currGlyph = None
  
      def read_classes(self, fname, classproperties=False):
          doc = et.parse(fname)
          for c in doc.findall('.//class'):
              class_name = c.get('name')
              m = re.search('\[(\d+)\]$', class_name)
              # support fixedclasses like make_gdl.pl via AP.pm
              if m:
                  class_nm = class_name[0:m.start()]
                  ix = int(m.group(1))
              else:
                  class_nm = class_name
                  ix = None
              cl = self.classes.setdefault(class_nm, [])
              for e in c.get('exts', '').split() + [""]:
                  for g in c.text.split():
                      if g+e in self.glyphs or (e == '' and g.startswith('@')):
                          if ix:
                              cl.insert(ix, g+e)
                          else:
                              cl.append(g+e)
          if not classproperties:
              return
          for c in doc.findall('.//property'):
              for e in c.get('exts', '').split() + [""]:
                  for g in c.text.split():
                      if g+e in self.glyphs:
                          cname = c.get('name') + "_" + c.get('value')
                          self.classes.setdefault(cname, []).append(g+e)
                      
      def make_classes(self, ligmode) :
          for name, g in self.glyphs.items() :
              # pull off suffix and make classes
              # TODO: handle ligatures
              base = name
              if ligmode is None or 'comp' not in ligmode or "_" not in name:
                  pos = base.rfind('.')
                  while pos > 0 :
                      old_base = base
                      ext = base[pos+1:]
                      base = base[:pos]
                      ext_class_nm = "c_" + ext
                      if base in self.glyphs and old_base in self.glyphs:
                          glyph_lst = self.classes.setdefault(ext_class_nm, [])
                          if not old_base in glyph_lst:
                              glyph_lst.append(old_base)
                              self.classes.setdefault("cno_" + ext, []).append(base)
                      pos = base.rfind('.')
              if ligmode is not None and "_" in name:
                  comps = name.split("_")
                  if "comp" in ligmode or "." not in comps[-1]:
                      base = comps.pop(-1 if "last" in ligmode else 0)
                      cname = base.replace(".", "_")
                      noname = "_".join(comps)
                      if base in self.glyphs and noname in self.glyphs:
                          glyph_lst = self.classes.setdefault("clig_"+cname, [])
                          if name not in glyph_lst:
                              glyph_lst.append(name)
                              self.classes.setdefault("cligno_"+cname, []).append(noname)
              if g.is_mark :
                  self.classes.setdefault('GDEF_marks', []).append(name)
              else :
                  self.classes.setdefault('GDEF_bases', []).append(name)
  
      def make_marks(self) :
          for name, g in self.glyphs.items() :
              g.decide_if_mark()
  
      def order_classes(self):
          # return ordered list of classnames as desired for FEA
  
          # Start with alphabetical then correct:
          #   1. Put classes like "cno_whatever" adjacent to "c_whatever"
          #   2. Classes can be defined in terms of other classes but FEA requires that
          #      classes be defined before they can be referenced.
  
          def sortkey(x):
              key1 = 'c_' + x[4:] if x.startswith('cno_') else x
              return (key1, x)
  
          classes = sorted(self.classes.keys(), key=sortkey)
          links = {}  # key = classname; value = list of other classes that include this one
          counts = {} # key = classname; value = count of un-output classes that this class includes
          for name in classes:
              y = [c[1:] for c in self.classes[name] if c.startswith('@')]  #list of included classes
              counts[name] = len(y)
              for c in y:
                  links.setdefault(c, []).append(name)
  
          outclasses = []
          while len(classes) > 0:
              foundone = False
              for name in classes:
                  if counts[name] == 0:
                      foundone = True
                      # output this class
                      outclasses.append(name)
                      classes.remove(name)
                      # adjust counts of classes that include this one
                      if name in links:
                          for n in links[name]:
                              counts[n] -= 1
                      # It may now be possible to output some we skipped earlier,
                      # so start over from the beginning of the list
                      break
              if not foundone:
                  # all remaining classes include un-output classes and thus there is a loop somewhere
                  raise ValueError("Class reference loop(s) found: " + ", ".join(classes))
          return outclasses
  
      def addComment(self, parser, text):
          cmt = parser.ast.Comment("# " + text, location=None)
          cmt.pretext = "\n"
          parser.add_statement(cmt)
  
      def append_classes(self, parser) :
          # normal glyph classes
          self.addComment(parser, "Main Classes")
          for name in self.order_classes():
              gc = parser.ast.GlyphClass(None, location=None)
              for g in self.classes[name] :
                  gc.append(g)
              gcd = parser.ast.GlyphClassDefinition(name, gc, location=None)
              parser.add_statement(gcd)
              parser.define_glyphclass(name, gcd)
  
      def _addGlyphsToClass(self, parser, glyphs, gc, anchor, definer):
          if len(glyphs) > 1 :
              val = parser.ast.GlyphClass(glyphs, location=None)
          else :
              val = parser.ast.GlyphName(glyphs[0], location=None)
          classdef = definer(gc, anchor, val, location=None)
          gc.addDefinition(classdef)
          parser.add_statement(classdef)
  
      def append_positions(self, parser):
          # create base and mark classes, add to fea file dicts and parser symbol table
          bclassdef_lst = []
          mclassdef_lst = []
          self.addComment(parser, "Positioning classes and statements")
          for ap_nm, glyphs_w_ap in self.all_aps.items() :
              self.addComment(parser, "AP: " + ap_nm)
              # e.g. all glyphs with U AP
              if not ap_nm.startswith("_"):
                  if any(not x.is_mark for x in glyphs_w_ap):
                      gcb = parser.set_baseclass(ap_nm)
                      parser.add_statement(gcb)
                  if any(x.is_mark for x in glyphs_w_ap):
                      gcm = parser.set_baseclass(ap_nm + "_MarkBase")
                      parser.add_statement(gcm)
              else:
                  gc = parser.set_markclass(ap_nm)
  
              # create lists of glyphs that use the same point (name and coordinates)
              # that can share a class definition
              anchor_cache = OrderedDict()
              markanchor_cache = OrderedDict()
              for g in glyphs_w_ap :
                  p = g.anchors[ap_nm]
                  if g.is_mark and not ap_nm.startswith("_"):
                      markanchor_cache.setdefault(p, []).append(g.name)
                  else:
                      anchor_cache.setdefault(p, []).append(g.name)
  
              if ap_nm.startswith("_"):
                  for p, glyphs_w_pt in anchor_cache.items():
                      anchor = parser.ast.Anchor(p[0], p[1], location=None)
                      self._addGlyphsToClass(parser, glyphs_w_pt, gc, anchor, parser.ast.MarkClassDefinition)
              else:
                  for p, glyphs_w_pt in anchor_cache.items():
                      anchor = parser.ast.Anchor(p[0], p[1], location=None)
                      self._addGlyphsToClass(parser, glyphs_w_pt, gcb, anchor, parser.ast.BaseClassDefinition)
                  for p, glyphs_w_pt in markanchor_cache.items():
                      anchor = parser.ast.Anchor(p[0], p[1], location=None)
                      self._addGlyphsToClass(parser, glyphs_w_pt, gcm, anchor, parser.ast.BaseClassDefinition)
  
  #TODO: provide more argument info
  argspec = [
      ('infile', {'nargs': '?', 'help': 'Input UFO or file'}, {'def': None, 'type': 'filename'}),
      ('-i', '--input', {'required': 'True', 'help': 'Fea file to merge'}, {}),
      ('-o', '--output', {'help': 'Output fea file'}, {}),
      ('-c', '--classfile', {'help': 'Classes file'}, {}),
      ('-L', '--ligmode', {'help': 'Parse ligatures: last - use last element as class name, first - use first element as class name, lastcomp, firstcomp - final variants are part of the component not the whole ligature'}, {}),
      ('-D', '--define', {'action': 'append', 'help': 'Add option definition to pass to fea code --define=var=val'}, {}),
      # ('--debug', {'help': 'Drop into pdb', 'action': 'store_true'}, {}),
      ('--classprops', {'help': 'Include property elements from classes file', 'action': 'store_true'}, {}),
      ('--omitaps', {'help': 'names of attachment points to omit (comma- or space-separated)', 'default': '', 'action': 'store'}, {})
  ]
  
  def doit(args) :
      defines = dict(x.split('=') for x in args.define) if args.define else {}
      font = Font(defines)
      # if args.debug:
      #     import pdb; pdb.set_trace()
      if "checkfix" not in args.params:
          args.paramsobj.sets["main"]["checkfix"] = "None"
      if args.infile is not None:
          font.readaps(args.infile, args.omitaps, args.paramsobj)
  
      font.make_marks()
      font.make_classes(args.ligmode)
      if args.classfile:
          font.read_classes(args.classfile, classproperties = args.classprops)
  
      p = feaplus_parser(None, font.glyphs, font.fontinfo, font.kerns, font.defines)
      doc_ufo = p.parse() # returns an empty ast.FeatureFile
  
      # Add goodies from the font
      font.append_classes(p)
      font.append_positions(p)
  
      # parse the input fea file
      if args.input :
          doc_fea = p.parse(args.input)
      else:
          doc_fea = doc_ufo
  
      # output as doc.asFea()
      if args.output :
          with open(args.output, "w") as of :
              of.write(doc_fea.asFea())
  
  def cmd(): execute(None, doit, argspec)
  if __name__ == '__main__': cmd()
  