• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from fontTools.misc.py23 import byteord, tobytes
2from fontTools.feaLib.error import FeatureLibError
3from fontTools.feaLib.location import FeatureLibLocation
4from fontTools.misc.encodingTools import getEncoding
5from collections import OrderedDict
6import itertools
7
8SHIFT = " " * 4
9
10__all__ = [
11    "Element",
12    "FeatureFile",
13    "Comment",
14    "GlyphName",
15    "GlyphClass",
16    "GlyphClassName",
17    "MarkClassName",
18    "AnonymousBlock",
19    "Block",
20    "FeatureBlock",
21    "NestedBlock",
22    "LookupBlock",
23    "GlyphClassDefinition",
24    "GlyphClassDefStatement",
25    "MarkClass",
26    "MarkClassDefinition",
27    "AlternateSubstStatement",
28    "Anchor",
29    "AnchorDefinition",
30    "AttachStatement",
31    "AxisValueLocationStatement",
32    "BaseAxis",
33    "CVParametersNameStatement",
34    "ChainContextPosStatement",
35    "ChainContextSubstStatement",
36    "CharacterStatement",
37    "CursivePosStatement",
38    "ElidedFallbackName",
39    "ElidedFallbackNameID",
40    "Expression",
41    "FeatureNameStatement",
42    "FeatureReferenceStatement",
43    "FontRevisionStatement",
44    "HheaField",
45    "IgnorePosStatement",
46    "IgnoreSubstStatement",
47    "IncludeStatement",
48    "LanguageStatement",
49    "LanguageSystemStatement",
50    "LigatureCaretByIndexStatement",
51    "LigatureCaretByPosStatement",
52    "LigatureSubstStatement",
53    "LookupFlagStatement",
54    "LookupReferenceStatement",
55    "MarkBasePosStatement",
56    "MarkLigPosStatement",
57    "MarkMarkPosStatement",
58    "MultipleSubstStatement",
59    "NameRecord",
60    "OS2Field",
61    "PairPosStatement",
62    "ReverseChainSingleSubstStatement",
63    "ScriptStatement",
64    "SinglePosStatement",
65    "SingleSubstStatement",
66    "SizeParameters",
67    "Statement",
68    "STATAxisValueStatement",
69    "STATDesignAxisStatement",
70    "STATNameStatement",
71    "SubtableStatement",
72    "TableBlock",
73    "ValueRecord",
74    "ValueRecordDefinition",
75    "VheaField",
76]
77
78
79def deviceToString(device):
80    if device is None:
81        return "<device NULL>"
82    else:
83        return "<device %s>" % ", ".join("%d %d" % t for t in device)
84
85
86fea_keywords = set(
87    [
88        "anchor",
89        "anchordef",
90        "anon",
91        "anonymous",
92        "by",
93        "contour",
94        "cursive",
95        "device",
96        "enum",
97        "enumerate",
98        "excludedflt",
99        "exclude_dflt",
100        "feature",
101        "from",
102        "ignore",
103        "ignorebaseglyphs",
104        "ignoreligatures",
105        "ignoremarks",
106        "include",
107        "includedflt",
108        "include_dflt",
109        "language",
110        "languagesystem",
111        "lookup",
112        "lookupflag",
113        "mark",
114        "markattachmenttype",
115        "markclass",
116        "nameid",
117        "null",
118        "parameters",
119        "pos",
120        "position",
121        "required",
122        "righttoleft",
123        "reversesub",
124        "rsub",
125        "script",
126        "sub",
127        "substitute",
128        "subtable",
129        "table",
130        "usemarkfilteringset",
131        "useextension",
132        "valuerecorddef",
133        "base",
134        "gdef",
135        "head",
136        "hhea",
137        "name",
138        "vhea",
139        "vmtx",
140    ]
141)
142
143
144def asFea(g):
145    if hasattr(g, "asFea"):
146        return g.asFea()
147    elif isinstance(g, tuple) and len(g) == 2:
148        return asFea(g[0]) + " - " + asFea(g[1])  # a range
149    elif g.lower() in fea_keywords:
150        return "\\" + g
151    else:
152        return g
153
154
155class Element(object):
156    """A base class representing "something" in a feature file."""
157
158    def __init__(self, location=None):
159        #: location of this element as a `FeatureLibLocation` object.
160        if location and not isinstance(location, FeatureLibLocation):
161            location = FeatureLibLocation(*location)
162        self.location = location
163
164    def build(self, builder):
165        pass
166
167    def asFea(self, indent=""):
168        """Returns this element as a string of feature code. For block-type
169        elements (such as :class:`FeatureBlock`), the `indent` string is
170        added to the start of each line in the output."""
171        raise NotImplementedError
172
173    def __str__(self):
174        return self.asFea()
175
176
177class Statement(Element):
178    pass
179
180
181class Expression(Element):
182    pass
183
184
185class Comment(Element):
186    """A comment in a feature file."""
187
188    def __init__(self, text, location=None):
189        super(Comment, self).__init__(location)
190        #: Text of the comment
191        self.text = text
192
193    def asFea(self, indent=""):
194        return self.text
195
196
197class NullGlyph(Expression):
198    """The NULL glyph, used in glyph deletion substitutions."""
199
200    def __init__(self, location=None):
201        Expression.__init__(self, location)
202        #: The name itself as a string
203
204    def glyphSet(self):
205        """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
206        return ()
207
208    def asFea(self, indent=""):
209        return "NULL"
210
211
212class GlyphName(Expression):
213    """A single glyph name, such as ``cedilla``."""
214
215    def __init__(self, glyph, location=None):
216        Expression.__init__(self, location)
217        #: The name itself as a string
218        self.glyph = glyph
219
220    def glyphSet(self):
221        """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
222        return (self.glyph,)
223
224    def asFea(self, indent=""):
225        return asFea(self.glyph)
226
227
228class GlyphClass(Expression):
229    """A glyph class, such as ``[acute cedilla grave]``."""
230
231    def __init__(self, glyphs=None, location=None):
232        Expression.__init__(self, location)
233        #: The list of glyphs in this class, as :class:`GlyphName` objects.
234        self.glyphs = glyphs if glyphs is not None else []
235        self.original = []
236        self.curr = 0
237
238    def glyphSet(self):
239        """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
240        return tuple(self.glyphs)
241
242    def asFea(self, indent=""):
243        if len(self.original):
244            if self.curr < len(self.glyphs):
245                self.original.extend(self.glyphs[self.curr :])
246                self.curr = len(self.glyphs)
247            return "[" + " ".join(map(asFea, self.original)) + "]"
248        else:
249            return "[" + " ".join(map(asFea, self.glyphs)) + "]"
250
251    def extend(self, glyphs):
252        """Add a list of :class:`GlyphName` objects to the class."""
253        self.glyphs.extend(glyphs)
254
255    def append(self, glyph):
256        """Add a single :class:`GlyphName` object to the class."""
257        self.glyphs.append(glyph)
258
259    def add_range(self, start, end, glyphs):
260        """Add a range (e.g. ``A-Z``) to the class. ``start`` and ``end``
261        are either :class:`GlyphName` objects or strings representing the
262        start and end glyphs in the class, and ``glyphs`` is the full list of
263        :class:`GlyphName` objects in the range."""
264        if self.curr < len(self.glyphs):
265            self.original.extend(self.glyphs[self.curr :])
266        self.original.append((start, end))
267        self.glyphs.extend(glyphs)
268        self.curr = len(self.glyphs)
269
270    def add_cid_range(self, start, end, glyphs):
271        """Add a range to the class by glyph ID. ``start`` and ``end`` are the
272        initial and final IDs, and ``glyphs`` is the full list of
273        :class:`GlyphName` objects in the range."""
274        if self.curr < len(self.glyphs):
275            self.original.extend(self.glyphs[self.curr :])
276        self.original.append(("\\{}".format(start), "\\{}".format(end)))
277        self.glyphs.extend(glyphs)
278        self.curr = len(self.glyphs)
279
280    def add_class(self, gc):
281        """Add glyphs from the given :class:`GlyphClassName` object to the
282        class."""
283        if self.curr < len(self.glyphs):
284            self.original.extend(self.glyphs[self.curr :])
285        self.original.append(gc)
286        self.glyphs.extend(gc.glyphSet())
287        self.curr = len(self.glyphs)
288
289
290class GlyphClassName(Expression):
291    """A glyph class name, such as ``@FRENCH_MARKS``. This must be instantiated
292    with a :class:`GlyphClassDefinition` object."""
293
294    def __init__(self, glyphclass, location=None):
295        Expression.__init__(self, location)
296        assert isinstance(glyphclass, GlyphClassDefinition)
297        self.glyphclass = glyphclass
298
299    def glyphSet(self):
300        """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
301        return tuple(self.glyphclass.glyphSet())
302
303    def asFea(self, indent=""):
304        return "@" + self.glyphclass.name
305
306
307class MarkClassName(Expression):
308    """A mark class name, such as ``@FRENCH_MARKS`` defined with ``markClass``.
309    This must be instantiated with a :class:`MarkClass` object."""
310
311    def __init__(self, markClass, location=None):
312        Expression.__init__(self, location)
313        assert isinstance(markClass, MarkClass)
314        self.markClass = markClass
315
316    def glyphSet(self):
317        """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
318        return self.markClass.glyphSet()
319
320    def asFea(self, indent=""):
321        return "@" + self.markClass.name
322
323
324class AnonymousBlock(Statement):
325    """An anonymous data block."""
326
327    def __init__(self, tag, content, location=None):
328        Statement.__init__(self, location)
329        self.tag = tag  #: string containing the block's "tag"
330        self.content = content  #: block data as string
331
332    def asFea(self, indent=""):
333        res = "anon {} {{\n".format(self.tag)
334        res += self.content
335        res += "}} {};\n\n".format(self.tag)
336        return res
337
338
339class Block(Statement):
340    """A block of statements: feature, lookup, etc."""
341
342    def __init__(self, location=None):
343        Statement.__init__(self, location)
344        self.statements = []  #: Statements contained in the block
345
346    def build(self, builder):
347        """When handed a 'builder' object of comparable interface to
348        :class:`fontTools.feaLib.builder`, walks the statements in this
349        block, calling the builder callbacks."""
350        for s in self.statements:
351            s.build(builder)
352
353    def asFea(self, indent=""):
354        indent += SHIFT
355        return (
356            indent
357            + ("\n" + indent).join([s.asFea(indent=indent) for s in self.statements])
358            + "\n"
359        )
360
361
362class FeatureFile(Block):
363    """The top-level element of the syntax tree, containing the whole feature
364    file in its ``statements`` attribute."""
365
366    def __init__(self):
367        Block.__init__(self, location=None)
368        self.markClasses = {}  # name --> ast.MarkClass
369
370    def asFea(self, indent=""):
371        return "\n".join(s.asFea(indent=indent) for s in self.statements)
372
373
374class FeatureBlock(Block):
375    """A named feature block."""
376
377    def __init__(self, name, use_extension=False, location=None):
378        Block.__init__(self, location)
379        self.name, self.use_extension = name, use_extension
380
381    def build(self, builder):
382        """Call the ``start_feature`` callback on the builder object, visit
383        all the statements in this feature, and then call ``end_feature``."""
384        # TODO(sascha): Handle use_extension.
385        builder.start_feature(self.location, self.name)
386        # language exclude_dflt statements modify builder.features_
387        # limit them to this block with temporary builder.features_
388        features = builder.features_
389        builder.features_ = {}
390        Block.build(self, builder)
391        for key, value in builder.features_.items():
392            features.setdefault(key, []).extend(value)
393        builder.features_ = features
394        builder.end_feature()
395
396    def asFea(self, indent=""):
397        res = indent + "feature %s " % self.name.strip()
398        if self.use_extension:
399            res += "useExtension "
400        res += "{\n"
401        res += Block.asFea(self, indent=indent)
402        res += indent + "} %s;\n" % self.name.strip()
403        return res
404
405
406class NestedBlock(Block):
407    """A block inside another block, for example when found inside a
408    ``cvParameters`` block."""
409
410    def __init__(self, tag, block_name, location=None):
411        Block.__init__(self, location)
412        self.tag = tag
413        self.block_name = block_name
414
415    def build(self, builder):
416        Block.build(self, builder)
417        if self.block_name == "ParamUILabelNameID":
418            builder.add_to_cv_num_named_params(self.tag)
419
420    def asFea(self, indent=""):
421        res = "{}{} {{\n".format(indent, self.block_name)
422        res += Block.asFea(self, indent=indent)
423        res += "{}}};\n".format(indent)
424        return res
425
426
427class LookupBlock(Block):
428    """A named lookup, containing ``statements``."""
429
430    def __init__(self, name, use_extension=False, location=None):
431        Block.__init__(self, location)
432        self.name, self.use_extension = name, use_extension
433
434    def build(self, builder):
435        # TODO(sascha): Handle use_extension.
436        builder.start_lookup_block(self.location, self.name)
437        Block.build(self, builder)
438        builder.end_lookup_block()
439
440    def asFea(self, indent=""):
441        res = "lookup {} ".format(self.name)
442        if self.use_extension:
443            res += "useExtension "
444        res += "{\n"
445        res += Block.asFea(self, indent=indent)
446        res += "{}}} {};\n".format(indent, self.name)
447        return res
448
449
450class TableBlock(Block):
451    """A ``table ... { }`` block."""
452
453    def __init__(self, name, location=None):
454        Block.__init__(self, location)
455        self.name = name
456
457    def asFea(self, indent=""):
458        res = "table {} {{\n".format(self.name.strip())
459        res += super(TableBlock, self).asFea(indent=indent)
460        res += "}} {};\n".format(self.name.strip())
461        return res
462
463
464class GlyphClassDefinition(Statement):
465    """Example: ``@UPPERCASE = [A-Z];``."""
466
467    def __init__(self, name, glyphs, location=None):
468        Statement.__init__(self, location)
469        self.name = name  #: class name as a string, without initial ``@``
470        self.glyphs = glyphs  #: a :class:`GlyphClass` object
471
472    def glyphSet(self):
473        """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
474        return tuple(self.glyphs.glyphSet())
475
476    def asFea(self, indent=""):
477        return "@" + self.name + " = " + self.glyphs.asFea() + ";"
478
479
480class GlyphClassDefStatement(Statement):
481    """Example: ``GlyphClassDef @UPPERCASE, [B], [C], [D];``. The parameters
482    must be either :class:`GlyphClass` or :class:`GlyphClassName` objects, or
483    ``None``."""
484
485    def __init__(
486        self, baseGlyphs, markGlyphs, ligatureGlyphs, componentGlyphs, location=None
487    ):
488        Statement.__init__(self, location)
489        self.baseGlyphs, self.markGlyphs = (baseGlyphs, markGlyphs)
490        self.ligatureGlyphs = ligatureGlyphs
491        self.componentGlyphs = componentGlyphs
492
493    def build(self, builder):
494        """Calls the builder's ``add_glyphClassDef`` callback."""
495        base = self.baseGlyphs.glyphSet() if self.baseGlyphs else tuple()
496        liga = self.ligatureGlyphs.glyphSet() if self.ligatureGlyphs else tuple()
497        mark = self.markGlyphs.glyphSet() if self.markGlyphs else tuple()
498        comp = self.componentGlyphs.glyphSet() if self.componentGlyphs else tuple()
499        builder.add_glyphClassDef(self.location, base, liga, mark, comp)
500
501    def asFea(self, indent=""):
502        return "GlyphClassDef {}, {}, {}, {};".format(
503            self.baseGlyphs.asFea() if self.baseGlyphs else "",
504            self.ligatureGlyphs.asFea() if self.ligatureGlyphs else "",
505            self.markGlyphs.asFea() if self.markGlyphs else "",
506            self.componentGlyphs.asFea() if self.componentGlyphs else "",
507        )
508
509
510class MarkClass(object):
511    """One `or more` ``markClass`` statements for the same mark class.
512
513    While glyph classes can be defined only once, the feature file format
514    allows expanding mark classes with multiple definitions, each using
515    different glyphs and anchors. The following are two ``MarkClassDefinitions``
516    for the same ``MarkClass``::
517
518        markClass [acute grave] <anchor 350 800> @FRENCH_ACCENTS;
519        markClass [cedilla] <anchor 350 -200> @FRENCH_ACCENTS;
520
521    The ``MarkClass`` object is therefore just a container for a list of
522    :class:`MarkClassDefinition` statements.
523    """
524
525    def __init__(self, name):
526        self.name = name
527        self.definitions = []
528        self.glyphs = OrderedDict()  # glyph --> ast.MarkClassDefinitions
529
530    def addDefinition(self, definition):
531        """Add a :class:`MarkClassDefinition` statement to this mark class."""
532        assert isinstance(definition, MarkClassDefinition)
533        self.definitions.append(definition)
534        for glyph in definition.glyphSet():
535            if glyph in self.glyphs:
536                otherLoc = self.glyphs[glyph].location
537                if otherLoc is None:
538                    end = ""
539                else:
540                    end = f" at {otherLoc}"
541                raise FeatureLibError(
542                    "Glyph %s already defined%s" % (glyph, end), definition.location
543                )
544            self.glyphs[glyph] = definition
545
546    def glyphSet(self):
547        """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
548        return tuple(self.glyphs.keys())
549
550    def asFea(self, indent=""):
551        res = "\n".join(d.asFea() for d in self.definitions)
552        return res
553
554
555class MarkClassDefinition(Statement):
556    """A single ``markClass`` statement. The ``markClass`` should be a
557    :class:`MarkClass` object, the ``anchor`` an :class:`Anchor` object,
558    and the ``glyphs`` parameter should be a `glyph-containing object`_ .
559
560    Example:
561
562        .. code:: python
563
564            mc = MarkClass("FRENCH_ACCENTS")
565            mc.addDefinition( MarkClassDefinition(mc, Anchor(350, 800),
566                GlyphClass([ GlyphName("acute"), GlyphName("grave") ])
567            ) )
568            mc.addDefinition( MarkClassDefinition(mc, Anchor(350, -200),
569                GlyphClass([ GlyphName("cedilla") ])
570            ) )
571
572            mc.asFea()
573            # markClass [acute grave] <anchor 350 800> @FRENCH_ACCENTS;
574            # markClass [cedilla] <anchor 350 -200> @FRENCH_ACCENTS;
575
576    """
577
578    def __init__(self, markClass, anchor, glyphs, location=None):
579        Statement.__init__(self, location)
580        assert isinstance(markClass, MarkClass)
581        assert isinstance(anchor, Anchor) and isinstance(glyphs, Expression)
582        self.markClass, self.anchor, self.glyphs = markClass, anchor, glyphs
583
584    def glyphSet(self):
585        """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
586        return self.glyphs.glyphSet()
587
588    def asFea(self, indent=""):
589        return "markClass {} {} @{};".format(
590            self.glyphs.asFea(), self.anchor.asFea(), self.markClass.name
591        )
592
593
594class AlternateSubstStatement(Statement):
595    """A ``sub ... from ...`` statement.
596
597    ``prefix``, ``glyph``, ``suffix`` and ``replacement`` should be lists of
598    `glyph-containing objects`_. ``glyph`` should be a `one element list`."""
599
600    def __init__(self, prefix, glyph, suffix, replacement, location=None):
601        Statement.__init__(self, location)
602        self.prefix, self.glyph, self.suffix = (prefix, glyph, suffix)
603        self.replacement = replacement
604
605    def build(self, builder):
606        """Calls the builder's ``add_alternate_subst`` callback."""
607        glyph = self.glyph.glyphSet()
608        assert len(glyph) == 1, glyph
609        glyph = list(glyph)[0]
610        prefix = [p.glyphSet() for p in self.prefix]
611        suffix = [s.glyphSet() for s in self.suffix]
612        replacement = self.replacement.glyphSet()
613        builder.add_alternate_subst(self.location, prefix, glyph, suffix, replacement)
614
615    def asFea(self, indent=""):
616        res = "sub "
617        if len(self.prefix) or len(self.suffix):
618            if len(self.prefix):
619                res += " ".join(map(asFea, self.prefix)) + " "
620            res += asFea(self.glyph) + "'"  # even though we really only use 1
621            if len(self.suffix):
622                res += " " + " ".join(map(asFea, self.suffix))
623        else:
624            res += asFea(self.glyph)
625        res += " from "
626        res += asFea(self.replacement)
627        res += ";"
628        return res
629
630
631class Anchor(Expression):
632    """An ``Anchor`` element, used inside a ``pos`` rule.
633
634    If a ``name`` is given, this will be used in preference to the coordinates.
635    Other values should be integer.
636    """
637
638    def __init__(
639        self,
640        x,
641        y,
642        name=None,
643        contourpoint=None,
644        xDeviceTable=None,
645        yDeviceTable=None,
646        location=None,
647    ):
648        Expression.__init__(self, location)
649        self.name = name
650        self.x, self.y, self.contourpoint = x, y, contourpoint
651        self.xDeviceTable, self.yDeviceTable = xDeviceTable, yDeviceTable
652
653    def asFea(self, indent=""):
654        if self.name is not None:
655            return "<anchor {}>".format(self.name)
656        res = "<anchor {} {}".format(self.x, self.y)
657        if self.contourpoint:
658            res += " contourpoint {}".format(self.contourpoint)
659        if self.xDeviceTable or self.yDeviceTable:
660            res += " "
661            res += deviceToString(self.xDeviceTable)
662            res += " "
663            res += deviceToString(self.yDeviceTable)
664        res += ">"
665        return res
666
667
668class AnchorDefinition(Statement):
669    """A named anchor definition. (2.e.viii). ``name`` should be a string."""
670
671    def __init__(self, name, x, y, contourpoint=None, location=None):
672        Statement.__init__(self, location)
673        self.name, self.x, self.y, self.contourpoint = name, x, y, contourpoint
674
675    def asFea(self, indent=""):
676        res = "anchorDef {} {}".format(self.x, self.y)
677        if self.contourpoint:
678            res += " contourpoint {}".format(self.contourpoint)
679        res += " {};".format(self.name)
680        return res
681
682
683class AttachStatement(Statement):
684    """A ``GDEF`` table ``Attach`` statement."""
685
686    def __init__(self, glyphs, contourPoints, location=None):
687        Statement.__init__(self, location)
688        self.glyphs = glyphs  #: A `glyph-containing object`_
689        self.contourPoints = contourPoints  #: A list of integer contour points
690
691    def build(self, builder):
692        """Calls the builder's ``add_attach_points`` callback."""
693        glyphs = self.glyphs.glyphSet()
694        builder.add_attach_points(self.location, glyphs, self.contourPoints)
695
696    def asFea(self, indent=""):
697        return "Attach {} {};".format(
698            self.glyphs.asFea(), " ".join(str(c) for c in self.contourPoints)
699        )
700
701
702class ChainContextPosStatement(Statement):
703    """A chained contextual positioning statement.
704
705    ``prefix``, ``glyphs``, and ``suffix`` should be lists of
706    `glyph-containing objects`_ .
707
708    ``lookups`` should be a list of elements representing what lookups
709    to apply at each glyph position. Each element should be a
710    :class:`LookupBlock` to apply a single chaining lookup at the given
711    position, a list of :class:`LookupBlock`\ s to apply multiple
712    lookups, or ``None`` to apply no lookup. The length of the outer
713    list should equal the length of ``glyphs``; the inner lists can be
714    of variable length."""
715
716    def __init__(self, prefix, glyphs, suffix, lookups, location=None):
717        Statement.__init__(self, location)
718        self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix
719        self.lookups = list(lookups)
720        for i, lookup in enumerate(lookups):
721            if lookup:
722                try:
723                    (_ for _ in lookup)
724                except TypeError:
725                    self.lookups[i] = [lookup]
726
727    def build(self, builder):
728        """Calls the builder's ``add_chain_context_pos`` callback."""
729        prefix = [p.glyphSet() for p in self.prefix]
730        glyphs = [g.glyphSet() for g in self.glyphs]
731        suffix = [s.glyphSet() for s in self.suffix]
732        builder.add_chain_context_pos(
733            self.location, prefix, glyphs, suffix, self.lookups
734        )
735
736    def asFea(self, indent=""):
737        res = "pos "
738        if (
739            len(self.prefix)
740            or len(self.suffix)
741            or any([x is not None for x in self.lookups])
742        ):
743            if len(self.prefix):
744                res += " ".join(g.asFea() for g in self.prefix) + " "
745            for i, g in enumerate(self.glyphs):
746                res += g.asFea() + "'"
747                if self.lookups[i]:
748                    for lu in self.lookups[i]:
749                        res += " lookup " + lu.name
750                if i < len(self.glyphs) - 1:
751                    res += " "
752            if len(self.suffix):
753                res += " " + " ".join(map(asFea, self.suffix))
754        else:
755            res += " ".join(map(asFea, self.glyph))
756        res += ";"
757        return res
758
759
760class ChainContextSubstStatement(Statement):
761    """A chained contextual substitution statement.
762
763    ``prefix``, ``glyphs``, and ``suffix`` should be lists of
764    `glyph-containing objects`_ .
765
766    ``lookups`` should be a list of elements representing what lookups
767    to apply at each glyph position. Each element should be a
768    :class:`LookupBlock` to apply a single chaining lookup at the given
769    position, a list of :class:`LookupBlock`\ s to apply multiple
770    lookups, or ``None`` to apply no lookup. The length of the outer
771    list should equal the length of ``glyphs``; the inner lists can be
772    of variable length."""
773
774    def __init__(self, prefix, glyphs, suffix, lookups, location=None):
775        Statement.__init__(self, location)
776        self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix
777        self.lookups = list(lookups)
778        for i, lookup in enumerate(lookups):
779            if lookup:
780                try:
781                    (_ for _ in lookup)
782                except TypeError:
783                    self.lookups[i] = [lookup]
784
785    def build(self, builder):
786        """Calls the builder's ``add_chain_context_subst`` callback."""
787        prefix = [p.glyphSet() for p in self.prefix]
788        glyphs = [g.glyphSet() for g in self.glyphs]
789        suffix = [s.glyphSet() for s in self.suffix]
790        builder.add_chain_context_subst(
791            self.location, prefix, glyphs, suffix, self.lookups
792        )
793
794    def asFea(self, indent=""):
795        res = "sub "
796        if (
797            len(self.prefix)
798            or len(self.suffix)
799            or any([x is not None for x in self.lookups])
800        ):
801            if len(self.prefix):
802                res += " ".join(g.asFea() for g in self.prefix) + " "
803            for i, g in enumerate(self.glyphs):
804                res += g.asFea() + "'"
805                if self.lookups[i]:
806                    for lu in self.lookups[i]:
807                        res += " lookup " + lu.name
808                if i < len(self.glyphs) - 1:
809                    res += " "
810            if len(self.suffix):
811                res += " " + " ".join(map(asFea, self.suffix))
812        else:
813            res += " ".join(map(asFea, self.glyph))
814        res += ";"
815        return res
816
817
818class CursivePosStatement(Statement):
819    """A cursive positioning statement. Entry and exit anchors can either
820    be :class:`Anchor` objects or ``None``."""
821
822    def __init__(self, glyphclass, entryAnchor, exitAnchor, location=None):
823        Statement.__init__(self, location)
824        self.glyphclass = glyphclass
825        self.entryAnchor, self.exitAnchor = entryAnchor, exitAnchor
826
827    def build(self, builder):
828        """Calls the builder object's ``add_cursive_pos`` callback."""
829        builder.add_cursive_pos(
830            self.location, self.glyphclass.glyphSet(), self.entryAnchor, self.exitAnchor
831        )
832
833    def asFea(self, indent=""):
834        entry = self.entryAnchor.asFea() if self.entryAnchor else "<anchor NULL>"
835        exit = self.exitAnchor.asFea() if self.exitAnchor else "<anchor NULL>"
836        return "pos cursive {} {} {};".format(self.glyphclass.asFea(), entry, exit)
837
838
839class FeatureReferenceStatement(Statement):
840    """Example: ``feature salt;``"""
841
842    def __init__(self, featureName, location=None):
843        Statement.__init__(self, location)
844        self.location, self.featureName = (location, featureName)
845
846    def build(self, builder):
847        """Calls the builder object's ``add_feature_reference`` callback."""
848        builder.add_feature_reference(self.location, self.featureName)
849
850    def asFea(self, indent=""):
851        return "feature {};".format(self.featureName)
852
853
854class IgnorePosStatement(Statement):
855    """An ``ignore pos`` statement, containing `one or more` contexts to ignore.
856
857    ``chainContexts`` should be a list of ``(prefix, glyphs, suffix)`` tuples,
858    with each of ``prefix``, ``glyphs`` and ``suffix`` being
859    `glyph-containing objects`_ ."""
860
861    def __init__(self, chainContexts, location=None):
862        Statement.__init__(self, location)
863        self.chainContexts = chainContexts
864
865    def build(self, builder):
866        """Calls the builder object's ``add_chain_context_pos`` callback on each
867        rule context."""
868        for prefix, glyphs, suffix in self.chainContexts:
869            prefix = [p.glyphSet() for p in prefix]
870            glyphs = [g.glyphSet() for g in glyphs]
871            suffix = [s.glyphSet() for s in suffix]
872            builder.add_chain_context_pos(self.location, prefix, glyphs, suffix, [])
873
874    def asFea(self, indent=""):
875        contexts = []
876        for prefix, glyphs, suffix in self.chainContexts:
877            res = ""
878            if len(prefix) or len(suffix):
879                if len(prefix):
880                    res += " ".join(map(asFea, prefix)) + " "
881                res += " ".join(g.asFea() + "'" for g in glyphs)
882                if len(suffix):
883                    res += " " + " ".join(map(asFea, suffix))
884            else:
885                res += " ".join(map(asFea, glyphs))
886            contexts.append(res)
887        return "ignore pos " + ", ".join(contexts) + ";"
888
889
890class IgnoreSubstStatement(Statement):
891    """An ``ignore sub`` statement, containing `one or more` contexts to ignore.
892
893    ``chainContexts`` should be a list of ``(prefix, glyphs, suffix)`` tuples,
894    with each of ``prefix``, ``glyphs`` and ``suffix`` being
895    `glyph-containing objects`_ ."""
896
897    def __init__(self, chainContexts, location=None):
898        Statement.__init__(self, location)
899        self.chainContexts = chainContexts
900
901    def build(self, builder):
902        """Calls the builder object's ``add_chain_context_subst`` callback on
903        each rule context."""
904        for prefix, glyphs, suffix in self.chainContexts:
905            prefix = [p.glyphSet() for p in prefix]
906            glyphs = [g.glyphSet() for g in glyphs]
907            suffix = [s.glyphSet() for s in suffix]
908            builder.add_chain_context_subst(self.location, prefix, glyphs, suffix, [])
909
910    def asFea(self, indent=""):
911        contexts = []
912        for prefix, glyphs, suffix in self.chainContexts:
913            res = ""
914            if len(prefix) or len(suffix):
915                if len(prefix):
916                    res += " ".join(map(asFea, prefix)) + " "
917                res += " ".join(g.asFea() + "'" for g in glyphs)
918                if len(suffix):
919                    res += " " + " ".join(map(asFea, suffix))
920            else:
921                res += " ".join(map(asFea, glyphs))
922            contexts.append(res)
923        return "ignore sub " + ", ".join(contexts) + ";"
924
925
926class IncludeStatement(Statement):
927    """An ``include()`` statement."""
928
929    def __init__(self, filename, location=None):
930        super(IncludeStatement, self).__init__(location)
931        self.filename = filename  #: String containing name of file to include
932
933    def build(self):
934        # TODO: consider lazy-loading the including parser/lexer?
935        raise FeatureLibError(
936            "Building an include statement is not implemented yet. "
937            "Instead, use Parser(..., followIncludes=True) for building.",
938            self.location,
939        )
940
941    def asFea(self, indent=""):
942        return indent + "include(%s);" % self.filename
943
944
945class LanguageStatement(Statement):
946    """A ``language`` statement within a feature."""
947
948    def __init__(self, language, include_default=True, required=False, location=None):
949        Statement.__init__(self, location)
950        assert len(language) == 4
951        self.language = language  #: A four-character language tag
952        self.include_default = include_default  #: If false, "exclude_dflt"
953        self.required = required
954
955    def build(self, builder):
956        """Call the builder object's ``set_language`` callback."""
957        builder.set_language(
958            location=self.location,
959            language=self.language,
960            include_default=self.include_default,
961            required=self.required,
962        )
963
964    def asFea(self, indent=""):
965        res = "language {}".format(self.language.strip())
966        if not self.include_default:
967            res += " exclude_dflt"
968        if self.required:
969            res += " required"
970        res += ";"
971        return res
972
973
974class LanguageSystemStatement(Statement):
975    """A top-level ``languagesystem`` statement."""
976
977    def __init__(self, script, language, location=None):
978        Statement.__init__(self, location)
979        self.script, self.language = (script, language)
980
981    def build(self, builder):
982        """Calls the builder object's ``add_language_system`` callback."""
983        builder.add_language_system(self.location, self.script, self.language)
984
985    def asFea(self, indent=""):
986        return "languagesystem {} {};".format(self.script, self.language.strip())
987
988
989class FontRevisionStatement(Statement):
990    """A ``head`` table ``FontRevision`` statement. ``revision`` should be a
991    number, and will be formatted to three significant decimal places."""
992
993    def __init__(self, revision, location=None):
994        Statement.__init__(self, location)
995        self.revision = revision
996
997    def build(self, builder):
998        builder.set_font_revision(self.location, self.revision)
999
1000    def asFea(self, indent=""):
1001        return "FontRevision {:.3f};".format(self.revision)
1002
1003
1004class LigatureCaretByIndexStatement(Statement):
1005    """A ``GDEF`` table ``LigatureCaretByIndex`` statement. ``glyphs`` should be
1006    a `glyph-containing object`_, and ``carets`` should be a list of integers."""
1007
1008    def __init__(self, glyphs, carets, location=None):
1009        Statement.__init__(self, location)
1010        self.glyphs, self.carets = (glyphs, carets)
1011
1012    def build(self, builder):
1013        """Calls the builder object's ``add_ligatureCaretByIndex_`` callback."""
1014        glyphs = self.glyphs.glyphSet()
1015        builder.add_ligatureCaretByIndex_(self.location, glyphs, set(self.carets))
1016
1017    def asFea(self, indent=""):
1018        return "LigatureCaretByIndex {} {};".format(
1019            self.glyphs.asFea(), " ".join(str(x) for x in self.carets)
1020        )
1021
1022
1023class LigatureCaretByPosStatement(Statement):
1024    """A ``GDEF`` table ``LigatureCaretByPos`` statement. ``glyphs`` should be
1025    a `glyph-containing object`_, and ``carets`` should be a list of integers."""
1026
1027    def __init__(self, glyphs, carets, location=None):
1028        Statement.__init__(self, location)
1029        self.glyphs, self.carets = (glyphs, carets)
1030
1031    def build(self, builder):
1032        """Calls the builder object's ``add_ligatureCaretByPos_`` callback."""
1033        glyphs = self.glyphs.glyphSet()
1034        builder.add_ligatureCaretByPos_(self.location, glyphs, set(self.carets))
1035
1036    def asFea(self, indent=""):
1037        return "LigatureCaretByPos {} {};".format(
1038            self.glyphs.asFea(), " ".join(str(x) for x in self.carets)
1039        )
1040
1041
1042class LigatureSubstStatement(Statement):
1043    """A chained contextual substitution statement.
1044
1045    ``prefix``, ``glyphs``, and ``suffix`` should be lists of
1046    `glyph-containing objects`_; ``replacement`` should be a single
1047    `glyph-containing object`_.
1048
1049    If ``forceChain`` is True, this is expressed as a chaining rule
1050    (e.g. ``sub f' i' by f_i``) even when no context is given."""
1051
1052    def __init__(self, prefix, glyphs, suffix, replacement, forceChain, location=None):
1053        Statement.__init__(self, location)
1054        self.prefix, self.glyphs, self.suffix = (prefix, glyphs, suffix)
1055        self.replacement, self.forceChain = replacement, forceChain
1056
1057    def build(self, builder):
1058        prefix = [p.glyphSet() for p in self.prefix]
1059        glyphs = [g.glyphSet() for g in self.glyphs]
1060        suffix = [s.glyphSet() for s in self.suffix]
1061        builder.add_ligature_subst(
1062            self.location, prefix, glyphs, suffix, self.replacement, self.forceChain
1063        )
1064
1065    def asFea(self, indent=""):
1066        res = "sub "
1067        if len(self.prefix) or len(self.suffix) or self.forceChain:
1068            if len(self.prefix):
1069                res += " ".join(g.asFea() for g in self.prefix) + " "
1070            res += " ".join(g.asFea() + "'" for g in self.glyphs)
1071            if len(self.suffix):
1072                res += " " + " ".join(g.asFea() for g in self.suffix)
1073        else:
1074            res += " ".join(g.asFea() for g in self.glyphs)
1075        res += " by "
1076        res += asFea(self.replacement)
1077        res += ";"
1078        return res
1079
1080
1081class LookupFlagStatement(Statement):
1082    """A ``lookupflag`` statement. The ``value`` should be an integer value
1083    representing the flags in use, but not including the ``markAttachment``
1084    class and ``markFilteringSet`` values, which must be specified as
1085    glyph-containing objects."""
1086
1087    def __init__(
1088        self, value=0, markAttachment=None, markFilteringSet=None, location=None
1089    ):
1090        Statement.__init__(self, location)
1091        self.value = value
1092        self.markAttachment = markAttachment
1093        self.markFilteringSet = markFilteringSet
1094
1095    def build(self, builder):
1096        """Calls the builder object's ``set_lookup_flag`` callback."""
1097        markAttach = None
1098        if self.markAttachment is not None:
1099            markAttach = self.markAttachment.glyphSet()
1100        markFilter = None
1101        if self.markFilteringSet is not None:
1102            markFilter = self.markFilteringSet.glyphSet()
1103        builder.set_lookup_flag(self.location, self.value, markAttach, markFilter)
1104
1105    def asFea(self, indent=""):
1106        res = []
1107        flags = ["RightToLeft", "IgnoreBaseGlyphs", "IgnoreLigatures", "IgnoreMarks"]
1108        curr = 1
1109        for i in range(len(flags)):
1110            if self.value & curr != 0:
1111                res.append(flags[i])
1112            curr = curr << 1
1113        if self.markAttachment is not None:
1114            res.append("MarkAttachmentType {}".format(self.markAttachment.asFea()))
1115        if self.markFilteringSet is not None:
1116            res.append("UseMarkFilteringSet {}".format(self.markFilteringSet.asFea()))
1117        if not res:
1118            res = ["0"]
1119        return "lookupflag {};".format(" ".join(res))
1120
1121
1122class LookupReferenceStatement(Statement):
1123    """Represents a ``lookup ...;`` statement to include a lookup in a feature.
1124
1125    The ``lookup`` should be a :class:`LookupBlock` object."""
1126
1127    def __init__(self, lookup, location=None):
1128        Statement.__init__(self, location)
1129        self.location, self.lookup = (location, lookup)
1130
1131    def build(self, builder):
1132        """Calls the builder object's ``add_lookup_call`` callback."""
1133        builder.add_lookup_call(self.lookup.name)
1134
1135    def asFea(self, indent=""):
1136        return "lookup {};".format(self.lookup.name)
1137
1138
1139class MarkBasePosStatement(Statement):
1140    """A mark-to-base positioning rule. The ``base`` should be a
1141    `glyph-containing object`_. The ``marks`` should be a list of
1142    (:class:`Anchor`, :class:`MarkClass`) tuples."""
1143
1144    def __init__(self, base, marks, location=None):
1145        Statement.__init__(self, location)
1146        self.base, self.marks = base, marks
1147
1148    def build(self, builder):
1149        """Calls the builder object's ``add_mark_base_pos`` callback."""
1150        builder.add_mark_base_pos(self.location, self.base.glyphSet(), self.marks)
1151
1152    def asFea(self, indent=""):
1153        res = "pos base {}".format(self.base.asFea())
1154        for a, m in self.marks:
1155            res += "\n" + indent + SHIFT + "{} mark @{}".format(a.asFea(), m.name)
1156        res += ";"
1157        return res
1158
1159
1160class MarkLigPosStatement(Statement):
1161    """A mark-to-ligature positioning rule. The ``ligatures`` must be a
1162    `glyph-containing object`_. The ``marks`` should be a list of lists: each
1163    element in the top-level list represents a component glyph, and is made
1164    up of a list of (:class:`Anchor`, :class:`MarkClass`) tuples representing
1165    mark attachment points for that position.
1166
1167    Example::
1168
1169        m1 = MarkClass("TOP_MARKS")
1170        m2 = MarkClass("BOTTOM_MARKS")
1171        # ... add definitions to mark classes...
1172
1173        glyph = GlyphName("lam_meem_jeem")
1174        marks = [
1175            [ (Anchor(625,1800), m1) ], # Attachments on 1st component (lam)
1176            [ (Anchor(376,-378), m2) ], # Attachments on 2nd component (meem)
1177            [ ]                         # No attachments on the jeem
1178        ]
1179        mlp = MarkLigPosStatement(glyph, marks)
1180
1181        mlp.asFea()
1182        # pos ligature lam_meem_jeem <anchor 625 1800> mark @TOP_MARKS
1183        # ligComponent <anchor 376 -378> mark @BOTTOM_MARKS;
1184
1185    """
1186
1187    def __init__(self, ligatures, marks, location=None):
1188        Statement.__init__(self, location)
1189        self.ligatures, self.marks = ligatures, marks
1190
1191    def build(self, builder):
1192        """Calls the builder object's ``add_mark_lig_pos`` callback."""
1193        builder.add_mark_lig_pos(self.location, self.ligatures.glyphSet(), self.marks)
1194
1195    def asFea(self, indent=""):
1196        res = "pos ligature {}".format(self.ligatures.asFea())
1197        ligs = []
1198        for l in self.marks:
1199            temp = ""
1200            if l is None or not len(l):
1201                temp = "\n" + indent + SHIFT * 2 + "<anchor NULL>"
1202            else:
1203                for a, m in l:
1204                    temp += (
1205                        "\n"
1206                        + indent
1207                        + SHIFT * 2
1208                        + "{} mark @{}".format(a.asFea(), m.name)
1209                    )
1210            ligs.append(temp)
1211        res += ("\n" + indent + SHIFT + "ligComponent").join(ligs)
1212        res += ";"
1213        return res
1214
1215
1216class MarkMarkPosStatement(Statement):
1217    """A mark-to-mark positioning rule. The ``baseMarks`` must be a
1218    `glyph-containing object`_. The ``marks`` should be a list of
1219    (:class:`Anchor`, :class:`MarkClass`) tuples."""
1220
1221    def __init__(self, baseMarks, marks, location=None):
1222        Statement.__init__(self, location)
1223        self.baseMarks, self.marks = baseMarks, marks
1224
1225    def build(self, builder):
1226        """Calls the builder object's ``add_mark_mark_pos`` callback."""
1227        builder.add_mark_mark_pos(self.location, self.baseMarks.glyphSet(), self.marks)
1228
1229    def asFea(self, indent=""):
1230        res = "pos mark {}".format(self.baseMarks.asFea())
1231        for a, m in self.marks:
1232            res += "\n" + indent + SHIFT + "{} mark @{}".format(a.asFea(), m.name)
1233        res += ";"
1234        return res
1235
1236
1237class MultipleSubstStatement(Statement):
1238    """A multiple substitution statement.
1239
1240    Args:
1241        prefix: a list of `glyph-containing objects`_.
1242        glyph: a single glyph-containing object.
1243        suffix: a list of glyph-containing objects.
1244        replacement: a list of glyph-containing objects.
1245        forceChain: If true, the statement is expressed as a chaining rule
1246            (e.g. ``sub f' i' by f_i``) even when no context is given.
1247    """
1248
1249    def __init__(
1250        self, prefix, glyph, suffix, replacement, forceChain=False, location=None
1251    ):
1252        Statement.__init__(self, location)
1253        self.prefix, self.glyph, self.suffix = prefix, glyph, suffix
1254        self.replacement = replacement
1255        self.forceChain = forceChain
1256
1257    def build(self, builder):
1258        """Calls the builder object's ``add_multiple_subst`` callback."""
1259        prefix = [p.glyphSet() for p in self.prefix]
1260        suffix = [s.glyphSet() for s in self.suffix]
1261        builder.add_multiple_subst(
1262            self.location, prefix, self.glyph, suffix, self.replacement, self.forceChain
1263        )
1264
1265    def asFea(self, indent=""):
1266        res = "sub "
1267        if len(self.prefix) or len(self.suffix) or self.forceChain:
1268            if len(self.prefix):
1269                res += " ".join(map(asFea, self.prefix)) + " "
1270            res += asFea(self.glyph) + "'"
1271            if len(self.suffix):
1272                res += " " + " ".join(map(asFea, self.suffix))
1273        else:
1274            res += asFea(self.glyph)
1275        replacement = self.replacement or [NullGlyph()]
1276        res += " by "
1277        res += " ".join(map(asFea, replacement))
1278        res += ";"
1279        return res
1280
1281
1282class PairPosStatement(Statement):
1283    """A pair positioning statement.
1284
1285    ``glyphs1`` and ``glyphs2`` should be `glyph-containing objects`_.
1286    ``valuerecord1`` should be a :class:`ValueRecord` object;
1287    ``valuerecord2`` should be either a :class:`ValueRecord` object or ``None``.
1288    If ``enumerated`` is true, then this is expressed as an
1289    `enumerated pair <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#6.b.ii>`_.
1290    """
1291
1292    def __init__(
1293        self,
1294        glyphs1,
1295        valuerecord1,
1296        glyphs2,
1297        valuerecord2,
1298        enumerated=False,
1299        location=None,
1300    ):
1301        Statement.__init__(self, location)
1302        self.enumerated = enumerated
1303        self.glyphs1, self.valuerecord1 = glyphs1, valuerecord1
1304        self.glyphs2, self.valuerecord2 = glyphs2, valuerecord2
1305
1306    def build(self, builder):
1307        """Calls a callback on the builder object:
1308
1309        * If the rule is enumerated, calls ``add_specific_pair_pos`` on each
1310          combination of first and second glyphs.
1311        * If the glyphs are both single :class:`GlyphName` objects, calls
1312          ``add_specific_pair_pos``.
1313        * Else, calls ``add_class_pair_pos``.
1314        """
1315        if self.enumerated:
1316            g = [self.glyphs1.glyphSet(), self.glyphs2.glyphSet()]
1317            for glyph1, glyph2 in itertools.product(*g):
1318                builder.add_specific_pair_pos(
1319                    self.location, glyph1, self.valuerecord1, glyph2, self.valuerecord2
1320                )
1321            return
1322
1323        is_specific = isinstance(self.glyphs1, GlyphName) and isinstance(
1324            self.glyphs2, GlyphName
1325        )
1326        if is_specific:
1327            builder.add_specific_pair_pos(
1328                self.location,
1329                self.glyphs1.glyph,
1330                self.valuerecord1,
1331                self.glyphs2.glyph,
1332                self.valuerecord2,
1333            )
1334        else:
1335            builder.add_class_pair_pos(
1336                self.location,
1337                self.glyphs1.glyphSet(),
1338                self.valuerecord1,
1339                self.glyphs2.glyphSet(),
1340                self.valuerecord2,
1341            )
1342
1343    def asFea(self, indent=""):
1344        res = "enum " if self.enumerated else ""
1345        if self.valuerecord2:
1346            res += "pos {} {} {} {};".format(
1347                self.glyphs1.asFea(),
1348                self.valuerecord1.asFea(),
1349                self.glyphs2.asFea(),
1350                self.valuerecord2.asFea(),
1351            )
1352        else:
1353            res += "pos {} {} {};".format(
1354                self.glyphs1.asFea(), self.glyphs2.asFea(), self.valuerecord1.asFea()
1355            )
1356        return res
1357
1358
1359class ReverseChainSingleSubstStatement(Statement):
1360    """A reverse chaining substitution statement. You don't see those every day.
1361
1362    Note the unusual argument order: ``suffix`` comes `before` ``glyphs``.
1363    ``old_prefix``, ``old_suffix``, ``glyphs`` and ``replacements`` should be
1364    lists of `glyph-containing objects`_. ``glyphs`` and ``replacements`` should
1365    be one-item lists.
1366    """
1367
1368    def __init__(self, old_prefix, old_suffix, glyphs, replacements, location=None):
1369        Statement.__init__(self, location)
1370        self.old_prefix, self.old_suffix = old_prefix, old_suffix
1371        self.glyphs = glyphs
1372        self.replacements = replacements
1373
1374    def build(self, builder):
1375        prefix = [p.glyphSet() for p in self.old_prefix]
1376        suffix = [s.glyphSet() for s in self.old_suffix]
1377        originals = self.glyphs[0].glyphSet()
1378        replaces = self.replacements[0].glyphSet()
1379        if len(replaces) == 1:
1380            replaces = replaces * len(originals)
1381        builder.add_reverse_chain_single_subst(
1382            self.location, prefix, suffix, dict(zip(originals, replaces))
1383        )
1384
1385    def asFea(self, indent=""):
1386        res = "rsub "
1387        if len(self.old_prefix) or len(self.old_suffix):
1388            if len(self.old_prefix):
1389                res += " ".join(asFea(g) for g in self.old_prefix) + " "
1390            res += " ".join(asFea(g) + "'" for g in self.glyphs)
1391            if len(self.old_suffix):
1392                res += " " + " ".join(asFea(g) for g in self.old_suffix)
1393        else:
1394            res += " ".join(map(asFea, self.glyphs))
1395        res += " by {};".format(" ".join(asFea(g) for g in self.replacements))
1396        return res
1397
1398
1399class SingleSubstStatement(Statement):
1400    """A single substitution statement.
1401
1402    Note the unusual argument order: ``prefix`` and suffix come `after`
1403    the replacement ``glyphs``. ``prefix``, ``suffix``, ``glyphs`` and
1404    ``replace`` should be lists of `glyph-containing objects`_. ``glyphs`` and
1405    ``replace`` should be one-item lists.
1406    """
1407
1408    def __init__(self, glyphs, replace, prefix, suffix, forceChain, location=None):
1409        Statement.__init__(self, location)
1410        self.prefix, self.suffix = prefix, suffix
1411        self.forceChain = forceChain
1412        self.glyphs = glyphs
1413        self.replacements = replace
1414
1415    def build(self, builder):
1416        """Calls the builder object's ``add_single_subst`` callback."""
1417        prefix = [p.glyphSet() for p in self.prefix]
1418        suffix = [s.glyphSet() for s in self.suffix]
1419        originals = self.glyphs[0].glyphSet()
1420        replaces = self.replacements[0].glyphSet()
1421        if len(replaces) == 1:
1422            replaces = replaces * len(originals)
1423        builder.add_single_subst(
1424            self.location,
1425            prefix,
1426            suffix,
1427            OrderedDict(zip(originals, replaces)),
1428            self.forceChain,
1429        )
1430
1431    def asFea(self, indent=""):
1432        res = "sub "
1433        if len(self.prefix) or len(self.suffix) or self.forceChain:
1434            if len(self.prefix):
1435                res += " ".join(asFea(g) for g in self.prefix) + " "
1436            res += " ".join(asFea(g) + "'" for g in self.glyphs)
1437            if len(self.suffix):
1438                res += " " + " ".join(asFea(g) for g in self.suffix)
1439        else:
1440            res += " ".join(asFea(g) for g in self.glyphs)
1441        res += " by {};".format(" ".join(asFea(g) for g in self.replacements))
1442        return res
1443
1444
1445class ScriptStatement(Statement):
1446    """A ``script`` statement."""
1447
1448    def __init__(self, script, location=None):
1449        Statement.__init__(self, location)
1450        self.script = script  #: the script code
1451
1452    def build(self, builder):
1453        """Calls the builder's ``set_script`` callback."""
1454        builder.set_script(self.location, self.script)
1455
1456    def asFea(self, indent=""):
1457        return "script {};".format(self.script.strip())
1458
1459
1460class SinglePosStatement(Statement):
1461    """A single position statement. ``prefix`` and ``suffix`` should be
1462    lists of `glyph-containing objects`_.
1463
1464    ``pos`` should be a one-element list containing a (`glyph-containing object`_,
1465    :class:`ValueRecord`) tuple."""
1466
1467    def __init__(self, pos, prefix, suffix, forceChain, location=None):
1468        Statement.__init__(self, location)
1469        self.pos, self.prefix, self.suffix = pos, prefix, suffix
1470        self.forceChain = forceChain
1471
1472    def build(self, builder):
1473        """Calls the builder object's ``add_single_pos`` callback."""
1474        prefix = [p.glyphSet() for p in self.prefix]
1475        suffix = [s.glyphSet() for s in self.suffix]
1476        pos = [(g.glyphSet(), value) for g, value in self.pos]
1477        builder.add_single_pos(self.location, prefix, suffix, pos, self.forceChain)
1478
1479    def asFea(self, indent=""):
1480        res = "pos "
1481        if len(self.prefix) or len(self.suffix) or self.forceChain:
1482            if len(self.prefix):
1483                res += " ".join(map(asFea, self.prefix)) + " "
1484            res += " ".join(
1485                [
1486                    asFea(x[0]) + "'" + ((" " + x[1].asFea()) if x[1] else "")
1487                    for x in self.pos
1488                ]
1489            )
1490            if len(self.suffix):
1491                res += " " + " ".join(map(asFea, self.suffix))
1492        else:
1493            res += " ".join(
1494                [asFea(x[0]) + " " + (x[1].asFea() if x[1] else "") for x in self.pos]
1495            )
1496        res += ";"
1497        return res
1498
1499
1500class SubtableStatement(Statement):
1501    """Represents a subtable break."""
1502
1503    def __init__(self, location=None):
1504        Statement.__init__(self, location)
1505
1506    def build(self, builder):
1507        """Calls the builder objects's ``add_subtable_break`` callback."""
1508        builder.add_subtable_break(self.location)
1509
1510    def asFea(self, indent=""):
1511        return "subtable;"
1512
1513
1514class ValueRecord(Expression):
1515    """Represents a value record."""
1516
1517    def __init__(
1518        self,
1519        xPlacement=None,
1520        yPlacement=None,
1521        xAdvance=None,
1522        yAdvance=None,
1523        xPlaDevice=None,
1524        yPlaDevice=None,
1525        xAdvDevice=None,
1526        yAdvDevice=None,
1527        vertical=False,
1528        location=None,
1529    ):
1530        Expression.__init__(self, location)
1531        self.xPlacement, self.yPlacement = (xPlacement, yPlacement)
1532        self.xAdvance, self.yAdvance = (xAdvance, yAdvance)
1533        self.xPlaDevice, self.yPlaDevice = (xPlaDevice, yPlaDevice)
1534        self.xAdvDevice, self.yAdvDevice = (xAdvDevice, yAdvDevice)
1535        self.vertical = vertical
1536
1537    def __eq__(self, other):
1538        return (
1539            self.xPlacement == other.xPlacement
1540            and self.yPlacement == other.yPlacement
1541            and self.xAdvance == other.xAdvance
1542            and self.yAdvance == other.yAdvance
1543            and self.xPlaDevice == other.xPlaDevice
1544            and self.xAdvDevice == other.xAdvDevice
1545        )
1546
1547    def __ne__(self, other):
1548        return not self.__eq__(other)
1549
1550    def __hash__(self):
1551        return (
1552            hash(self.xPlacement)
1553            ^ hash(self.yPlacement)
1554            ^ hash(self.xAdvance)
1555            ^ hash(self.yAdvance)
1556            ^ hash(self.xPlaDevice)
1557            ^ hash(self.yPlaDevice)
1558            ^ hash(self.xAdvDevice)
1559            ^ hash(self.yAdvDevice)
1560        )
1561
1562    def asFea(self, indent=""):
1563        if not self:
1564            return "<NULL>"
1565
1566        x, y = self.xPlacement, self.yPlacement
1567        xAdvance, yAdvance = self.xAdvance, self.yAdvance
1568        xPlaDevice, yPlaDevice = self.xPlaDevice, self.yPlaDevice
1569        xAdvDevice, yAdvDevice = self.xAdvDevice, self.yAdvDevice
1570        vertical = self.vertical
1571
1572        # Try format A, if possible.
1573        if x is None and y is None:
1574            if xAdvance is None and vertical:
1575                return str(yAdvance)
1576            elif yAdvance is None and not vertical:
1577                return str(xAdvance)
1578
1579        # Make any remaining None value 0 to avoid generating invalid records.
1580        x = x or 0
1581        y = y or 0
1582        xAdvance = xAdvance or 0
1583        yAdvance = yAdvance or 0
1584
1585        # Try format B, if possible.
1586        if (
1587            xPlaDevice is None
1588            and yPlaDevice is None
1589            and xAdvDevice is None
1590            and yAdvDevice is None
1591        ):
1592            return "<%s %s %s %s>" % (x, y, xAdvance, yAdvance)
1593
1594        # Last resort is format C.
1595        return "<%s %s %s %s %s %s %s %s>" % (
1596            x,
1597            y,
1598            xAdvance,
1599            yAdvance,
1600            deviceToString(xPlaDevice),
1601            deviceToString(yPlaDevice),
1602            deviceToString(xAdvDevice),
1603            deviceToString(yAdvDevice),
1604        )
1605
1606    def __bool__(self):
1607        return any(
1608            getattr(self, v) is not None
1609            for v in [
1610                "xPlacement",
1611                "yPlacement",
1612                "xAdvance",
1613                "yAdvance",
1614                "xPlaDevice",
1615                "yPlaDevice",
1616                "xAdvDevice",
1617                "yAdvDevice",
1618            ]
1619        )
1620
1621    __nonzero__ = __bool__
1622
1623
1624class ValueRecordDefinition(Statement):
1625    """Represents a named value record definition."""
1626
1627    def __init__(self, name, value, location=None):
1628        Statement.__init__(self, location)
1629        self.name = name  #: Value record name as string
1630        self.value = value  #: :class:`ValueRecord` object
1631
1632    def asFea(self, indent=""):
1633        return "valueRecordDef {} {};".format(self.value.asFea(), self.name)
1634
1635
1636def simplify_name_attributes(pid, eid, lid):
1637    if pid == 3 and eid == 1 and lid == 1033:
1638        return ""
1639    elif pid == 1 and eid == 0 and lid == 0:
1640        return "1"
1641    else:
1642        return "{} {} {}".format(pid, eid, lid)
1643
1644
1645class NameRecord(Statement):
1646    """Represents a name record. (`Section 9.e. <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.e>`_)"""
1647
1648    def __init__(self, nameID, platformID, platEncID, langID, string, location=None):
1649        Statement.__init__(self, location)
1650        self.nameID = nameID  #: Name ID as integer (e.g. 9 for designer's name)
1651        self.platformID = platformID  #: Platform ID as integer
1652        self.platEncID = platEncID  #: Platform encoding ID as integer
1653        self.langID = langID  #: Language ID as integer
1654        self.string = string  #: Name record value
1655
1656    def build(self, builder):
1657        """Calls the builder object's ``add_name_record`` callback."""
1658        builder.add_name_record(
1659            self.location,
1660            self.nameID,
1661            self.platformID,
1662            self.platEncID,
1663            self.langID,
1664            self.string,
1665        )
1666
1667    def asFea(self, indent=""):
1668        def escape(c, escape_pattern):
1669            # Also escape U+0022 QUOTATION MARK and U+005C REVERSE SOLIDUS
1670            if c >= 0x20 and c <= 0x7E and c not in (0x22, 0x5C):
1671                return chr(c)
1672            else:
1673                return escape_pattern % c
1674
1675        encoding = getEncoding(self.platformID, self.platEncID, self.langID)
1676        if encoding is None:
1677            raise FeatureLibError("Unsupported encoding", self.location)
1678        s = tobytes(self.string, encoding=encoding)
1679        if encoding == "utf_16_be":
1680            escaped_string = "".join(
1681                [
1682                    escape(byteord(s[i]) * 256 + byteord(s[i + 1]), r"\%04x")
1683                    for i in range(0, len(s), 2)
1684                ]
1685            )
1686        else:
1687            escaped_string = "".join([escape(byteord(b), r"\%02x") for b in s])
1688        plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID)
1689        if plat != "":
1690            plat += " "
1691        return 'nameid {} {}"{}";'.format(self.nameID, plat, escaped_string)
1692
1693
1694class FeatureNameStatement(NameRecord):
1695    """Represents a ``sizemenuname`` or ``name`` statement."""
1696
1697    def build(self, builder):
1698        """Calls the builder object's ``add_featureName`` callback."""
1699        NameRecord.build(self, builder)
1700        builder.add_featureName(self.nameID)
1701
1702    def asFea(self, indent=""):
1703        if self.nameID == "size":
1704            tag = "sizemenuname"
1705        else:
1706            tag = "name"
1707        plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID)
1708        if plat != "":
1709            plat += " "
1710        return '{} {}"{}";'.format(tag, plat, self.string)
1711
1712
1713class STATNameStatement(NameRecord):
1714    """Represents a STAT table ``name`` statement."""
1715
1716    def asFea(self, indent=""):
1717        plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID)
1718        if plat != "":
1719            plat += " "
1720        return 'name {}"{}";'.format(plat, self.string)
1721
1722
1723class SizeParameters(Statement):
1724    """A ``parameters`` statement."""
1725
1726    def __init__(self, DesignSize, SubfamilyID, RangeStart, RangeEnd, location=None):
1727        Statement.__init__(self, location)
1728        self.DesignSize = DesignSize
1729        self.SubfamilyID = SubfamilyID
1730        self.RangeStart = RangeStart
1731        self.RangeEnd = RangeEnd
1732
1733    def build(self, builder):
1734        """Calls the builder object's ``set_size_parameters`` callback."""
1735        builder.set_size_parameters(
1736            self.location,
1737            self.DesignSize,
1738            self.SubfamilyID,
1739            self.RangeStart,
1740            self.RangeEnd,
1741        )
1742
1743    def asFea(self, indent=""):
1744        res = "parameters {:.1f} {}".format(self.DesignSize, self.SubfamilyID)
1745        if self.RangeStart != 0 or self.RangeEnd != 0:
1746            res += " {} {}".format(int(self.RangeStart * 10), int(self.RangeEnd * 10))
1747        return res + ";"
1748
1749
1750class CVParametersNameStatement(NameRecord):
1751    """Represent a name statement inside a ``cvParameters`` block."""
1752
1753    def __init__(
1754        self, nameID, platformID, platEncID, langID, string, block_name, location=None
1755    ):
1756        NameRecord.__init__(
1757            self, nameID, platformID, platEncID, langID, string, location=location
1758        )
1759        self.block_name = block_name
1760
1761    def build(self, builder):
1762        """Calls the builder object's ``add_cv_parameter`` callback."""
1763        item = ""
1764        if self.block_name == "ParamUILabelNameID":
1765            item = "_{}".format(builder.cv_num_named_params_.get(self.nameID, 0))
1766        builder.add_cv_parameter(self.nameID)
1767        self.nameID = (self.nameID, self.block_name + item)
1768        NameRecord.build(self, builder)
1769
1770    def asFea(self, indent=""):
1771        plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID)
1772        if plat != "":
1773            plat += " "
1774        return 'name {}"{}";'.format(plat, self.string)
1775
1776
1777class CharacterStatement(Statement):
1778    """
1779    Statement used in cvParameters blocks of Character Variant features (cvXX).
1780    The Unicode value may be written with either decimal or hexadecimal
1781    notation. The value must be preceded by '0x' if it is a hexadecimal value.
1782    The largest Unicode value allowed is 0xFFFFFF.
1783    """
1784
1785    def __init__(self, character, tag, location=None):
1786        Statement.__init__(self, location)
1787        self.character = character
1788        self.tag = tag
1789
1790    def build(self, builder):
1791        """Calls the builder object's ``add_cv_character`` callback."""
1792        builder.add_cv_character(self.character, self.tag)
1793
1794    def asFea(self, indent=""):
1795        return "Character {:#x};".format(self.character)
1796
1797
1798class BaseAxis(Statement):
1799    """An axis definition, being either a ``VertAxis.BaseTagList/BaseScriptList``
1800    pair or a ``HorizAxis.BaseTagList/BaseScriptList`` pair."""
1801
1802    def __init__(self, bases, scripts, vertical, location=None):
1803        Statement.__init__(self, location)
1804        self.bases = bases  #: A list of baseline tag names as strings
1805        self.scripts = scripts  #: A list of script record tuplets (script tag, default baseline tag, base coordinate)
1806        self.vertical = vertical  #: Boolean; VertAxis if True, HorizAxis if False
1807
1808    def build(self, builder):
1809        """Calls the builder object's ``set_base_axis`` callback."""
1810        builder.set_base_axis(self.bases, self.scripts, self.vertical)
1811
1812    def asFea(self, indent=""):
1813        direction = "Vert" if self.vertical else "Horiz"
1814        scripts = [
1815            "{} {} {}".format(a[0], a[1], " ".join(map(str, a[2])))
1816            for a in self.scripts
1817        ]
1818        return "{}Axis.BaseTagList {};\n{}{}Axis.BaseScriptList {};".format(
1819            direction, " ".join(self.bases), indent, direction, ", ".join(scripts)
1820        )
1821
1822
1823class OS2Field(Statement):
1824    """An entry in the ``OS/2`` table. Most ``values`` should be numbers or
1825    strings, apart from when the key is ``UnicodeRange``, ``CodePageRange``
1826    or ``Panose``, in which case it should be an array of integers."""
1827
1828    def __init__(self, key, value, location=None):
1829        Statement.__init__(self, location)
1830        self.key = key
1831        self.value = value
1832
1833    def build(self, builder):
1834        """Calls the builder object's ``add_os2_field`` callback."""
1835        builder.add_os2_field(self.key, self.value)
1836
1837    def asFea(self, indent=""):
1838        def intarr2str(x):
1839            return " ".join(map(str, x))
1840
1841        numbers = (
1842            "FSType",
1843            "TypoAscender",
1844            "TypoDescender",
1845            "TypoLineGap",
1846            "winAscent",
1847            "winDescent",
1848            "XHeight",
1849            "CapHeight",
1850            "WeightClass",
1851            "WidthClass",
1852            "LowerOpSize",
1853            "UpperOpSize",
1854        )
1855        ranges = ("UnicodeRange", "CodePageRange")
1856        keywords = dict([(x.lower(), [x, str]) for x in numbers])
1857        keywords.update([(x.lower(), [x, intarr2str]) for x in ranges])
1858        keywords["panose"] = ["Panose", intarr2str]
1859        keywords["vendor"] = ["Vendor", lambda y: '"{}"'.format(y)]
1860        if self.key in keywords:
1861            return "{} {};".format(
1862                keywords[self.key][0], keywords[self.key][1](self.value)
1863            )
1864        return ""  # should raise exception
1865
1866
1867class HheaField(Statement):
1868    """An entry in the ``hhea`` table."""
1869
1870    def __init__(self, key, value, location=None):
1871        Statement.__init__(self, location)
1872        self.key = key
1873        self.value = value
1874
1875    def build(self, builder):
1876        """Calls the builder object's ``add_hhea_field`` callback."""
1877        builder.add_hhea_field(self.key, self.value)
1878
1879    def asFea(self, indent=""):
1880        fields = ("CaretOffset", "Ascender", "Descender", "LineGap")
1881        keywords = dict([(x.lower(), x) for x in fields])
1882        return "{} {};".format(keywords[self.key], self.value)
1883
1884
1885class VheaField(Statement):
1886    """An entry in the ``vhea`` table."""
1887
1888    def __init__(self, key, value, location=None):
1889        Statement.__init__(self, location)
1890        self.key = key
1891        self.value = value
1892
1893    def build(self, builder):
1894        """Calls the builder object's ``add_vhea_field`` callback."""
1895        builder.add_vhea_field(self.key, self.value)
1896
1897    def asFea(self, indent=""):
1898        fields = ("VertTypoAscender", "VertTypoDescender", "VertTypoLineGap")
1899        keywords = dict([(x.lower(), x) for x in fields])
1900        return "{} {};".format(keywords[self.key], self.value)
1901
1902
1903class STATDesignAxisStatement(Statement):
1904    """A STAT table Design Axis
1905
1906    Args:
1907        tag (str): a 4 letter axis tag
1908        axisOrder (int): an int
1909        names (list): a list of :class:`STATNameStatement` objects
1910    """
1911
1912    def __init__(self, tag, axisOrder, names, location=None):
1913        Statement.__init__(self, location)
1914        self.tag = tag
1915        self.axisOrder = axisOrder
1916        self.names = names
1917        self.location = location
1918
1919    def build(self, builder):
1920        builder.addDesignAxis(self, self.location)
1921
1922    def asFea(self, indent=""):
1923        indent += SHIFT
1924        res = f"DesignAxis {self.tag} {self.axisOrder} {{ \n"
1925        res += ("\n" + indent).join([s.asFea(indent=indent) for s in self.names]) + "\n"
1926        res += "};"
1927        return res
1928
1929
1930class ElidedFallbackName(Statement):
1931    """STAT table ElidedFallbackName
1932
1933    Args:
1934        names: a list of :class:`STATNameStatement` objects
1935    """
1936
1937    def __init__(self, names, location=None):
1938        Statement.__init__(self, location)
1939        self.names = names
1940        self.location = location
1941
1942    def build(self, builder):
1943        builder.setElidedFallbackName(self.names, self.location)
1944
1945    def asFea(self, indent=""):
1946        indent += SHIFT
1947        res = "ElidedFallbackName { \n"
1948        res += ("\n" + indent).join([s.asFea(indent=indent) for s in self.names]) + "\n"
1949        res += "};"
1950        return res
1951
1952
1953class ElidedFallbackNameID(Statement):
1954    """STAT table ElidedFallbackNameID
1955
1956    Args:
1957        value: an int pointing to an existing name table name ID
1958    """
1959
1960    def __init__(self, value, location=None):
1961        Statement.__init__(self, location)
1962        self.value = value
1963        self.location = location
1964
1965    def build(self, builder):
1966        builder.setElidedFallbackName(self.value, self.location)
1967
1968    def asFea(self, indent=""):
1969        return f"ElidedFallbackNameID {self.value};"
1970
1971
1972class STATAxisValueStatement(Statement):
1973    """A STAT table Axis Value Record
1974
1975    Args:
1976        names (list): a list of :class:`STATNameStatement` objects
1977        locations (list): a list of :class:`AxisValueLocationStatement` objects
1978        flags (int): an int
1979    """
1980
1981    def __init__(self, names, locations, flags, location=None):
1982        Statement.__init__(self, location)
1983        self.names = names
1984        self.locations = locations
1985        self.flags = flags
1986
1987    def build(self, builder):
1988        builder.addAxisValueRecord(self, self.location)
1989
1990    def asFea(self, indent=""):
1991        res = "AxisValue {\n"
1992        for location in self.locations:
1993            res += location.asFea()
1994
1995        for nameRecord in self.names:
1996            res += nameRecord.asFea()
1997            res += "\n"
1998
1999        if self.flags:
2000            flags = ["OlderSiblingFontAttribute", "ElidableAxisValueName"]
2001            flagStrings = []
2002            curr = 1
2003            for i in range(len(flags)):
2004                if self.flags & curr != 0:
2005                    flagStrings.append(flags[i])
2006                curr = curr << 1
2007            res += f"flag {' '.join(flagStrings)};\n"
2008        res += "};"
2009        return res
2010
2011
2012class AxisValueLocationStatement(Statement):
2013    """
2014    A STAT table Axis Value Location
2015
2016    Args:
2017        tag (str): a 4 letter axis tag
2018        values (list): a list of ints and/or floats
2019    """
2020
2021    def __init__(self, tag, values, location=None):
2022        Statement.__init__(self, location)
2023        self.tag = tag
2024        self.values = values
2025
2026    def asFea(self, res=""):
2027        res += f"location {self.tag} "
2028        res += f"{' '.join(str(i) for i in self.values)};\n"
2029        return res
2030