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