xref: /aosp_15_r20/external/fonttools/Lib/fontTools/feaLib/ast.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
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):
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            contexts.append(res)
921        return "ignore sub " + ", ".join(contexts) + ";"
922
923
924class IncludeStatement(Statement):
925    """An ``include()`` statement."""
926
927    def __init__(self, filename, location=None):
928        super(IncludeStatement, self).__init__(location)
929        self.filename = filename  #: String containing name of file to include
930
931    def build(self):
932        # TODO: consider lazy-loading the including parser/lexer?
933        raise FeatureLibError(
934            "Building an include statement is not implemented yet. "
935            "Instead, use Parser(..., followIncludes=True) for building.",
936            self.location,
937        )
938
939    def asFea(self, indent=""):
940        return indent + "include(%s);" % self.filename
941
942
943class LanguageStatement(Statement):
944    """A ``language`` statement within a feature."""
945
946    def __init__(self, language, include_default=True, required=False, location=None):
947        Statement.__init__(self, location)
948        assert len(language) == 4
949        self.language = language  #: A four-character language tag
950        self.include_default = include_default  #: If false, "exclude_dflt"
951        self.required = required
952
953    def build(self, builder):
954        """Call the builder object's ``set_language`` callback."""
955        builder.set_language(
956            location=self.location,
957            language=self.language,
958            include_default=self.include_default,
959            required=self.required,
960        )
961
962    def asFea(self, indent=""):
963        res = "language {}".format(self.language.strip())
964        if not self.include_default:
965            res += " exclude_dflt"
966        if self.required:
967            res += " required"
968        res += ";"
969        return res
970
971
972class LanguageSystemStatement(Statement):
973    """A top-level ``languagesystem`` statement."""
974
975    def __init__(self, script, language, location=None):
976        Statement.__init__(self, location)
977        self.script, self.language = (script, language)
978
979    def build(self, builder):
980        """Calls the builder object's ``add_language_system`` callback."""
981        builder.add_language_system(self.location, self.script, self.language)
982
983    def asFea(self, indent=""):
984        return "languagesystem {} {};".format(self.script, self.language.strip())
985
986
987class FontRevisionStatement(Statement):
988    """A ``head`` table ``FontRevision`` statement. ``revision`` should be a
989    number, and will be formatted to three significant decimal places."""
990
991    def __init__(self, revision, location=None):
992        Statement.__init__(self, location)
993        self.revision = revision
994
995    def build(self, builder):
996        builder.set_font_revision(self.location, self.revision)
997
998    def asFea(self, indent=""):
999        return "FontRevision {:.3f};".format(self.revision)
1000
1001
1002class LigatureCaretByIndexStatement(Statement):
1003    """A ``GDEF`` table ``LigatureCaretByIndex`` statement. ``glyphs`` should be
1004    a `glyph-containing object`_, and ``carets`` should be a list of integers."""
1005
1006    def __init__(self, glyphs, carets, location=None):
1007        Statement.__init__(self, location)
1008        self.glyphs, self.carets = (glyphs, carets)
1009
1010    def build(self, builder):
1011        """Calls the builder object's ``add_ligatureCaretByIndex_`` callback."""
1012        glyphs = self.glyphs.glyphSet()
1013        builder.add_ligatureCaretByIndex_(self.location, glyphs, set(self.carets))
1014
1015    def asFea(self, indent=""):
1016        return "LigatureCaretByIndex {} {};".format(
1017            self.glyphs.asFea(), " ".join(str(x) for x in self.carets)
1018        )
1019
1020
1021class LigatureCaretByPosStatement(Statement):
1022    """A ``GDEF`` table ``LigatureCaretByPos`` statement. ``glyphs`` should be
1023    a `glyph-containing object`_, and ``carets`` should be a list of integers."""
1024
1025    def __init__(self, glyphs, carets, location=None):
1026        Statement.__init__(self, location)
1027        self.glyphs, self.carets = (glyphs, carets)
1028
1029    def build(self, builder):
1030        """Calls the builder object's ``add_ligatureCaretByPos_`` callback."""
1031        glyphs = self.glyphs.glyphSet()
1032        builder.add_ligatureCaretByPos_(self.location, glyphs, set(self.carets))
1033
1034    def asFea(self, indent=""):
1035        return "LigatureCaretByPos {} {};".format(
1036            self.glyphs.asFea(), " ".join(str(x) for x in self.carets)
1037        )
1038
1039
1040class LigatureSubstStatement(Statement):
1041    """A chained contextual substitution statement.
1042
1043    ``prefix``, ``glyphs``, and ``suffix`` should be lists of
1044    `glyph-containing objects`_; ``replacement`` should be a single
1045    `glyph-containing object`_.
1046
1047    If ``forceChain`` is True, this is expressed as a chaining rule
1048    (e.g. ``sub f' i' by f_i``) even when no context is given."""
1049
1050    def __init__(self, prefix, glyphs, suffix, replacement, forceChain, location=None):
1051        Statement.__init__(self, location)
1052        self.prefix, self.glyphs, self.suffix = (prefix, glyphs, suffix)
1053        self.replacement, self.forceChain = replacement, forceChain
1054
1055    def build(self, builder):
1056        prefix = [p.glyphSet() for p in self.prefix]
1057        glyphs = [g.glyphSet() for g in self.glyphs]
1058        suffix = [s.glyphSet() for s in self.suffix]
1059        builder.add_ligature_subst(
1060            self.location, prefix, glyphs, suffix, self.replacement, self.forceChain
1061        )
1062
1063    def asFea(self, indent=""):
1064        res = "sub "
1065        if len(self.prefix) or len(self.suffix) or self.forceChain:
1066            if len(self.prefix):
1067                res += " ".join(g.asFea() for g in self.prefix) + " "
1068            res += " ".join(g.asFea() + "'" for g in self.glyphs)
1069            if len(self.suffix):
1070                res += " " + " ".join(g.asFea() for g in self.suffix)
1071        else:
1072            res += " ".join(g.asFea() for g in self.glyphs)
1073        res += " by "
1074        res += asFea(self.replacement)
1075        res += ";"
1076        return res
1077
1078
1079class LookupFlagStatement(Statement):
1080    """A ``lookupflag`` statement. The ``value`` should be an integer value
1081    representing the flags in use, but not including the ``markAttachment``
1082    class and ``markFilteringSet`` values, which must be specified as
1083    glyph-containing objects."""
1084
1085    def __init__(
1086        self, value=0, markAttachment=None, markFilteringSet=None, location=None
1087    ):
1088        Statement.__init__(self, location)
1089        self.value = value
1090        self.markAttachment = markAttachment
1091        self.markFilteringSet = markFilteringSet
1092
1093    def build(self, builder):
1094        """Calls the builder object's ``set_lookup_flag`` callback."""
1095        markAttach = None
1096        if self.markAttachment is not None:
1097            markAttach = self.markAttachment.glyphSet()
1098        markFilter = None
1099        if self.markFilteringSet is not None:
1100            markFilter = self.markFilteringSet.glyphSet()
1101        builder.set_lookup_flag(self.location, self.value, markAttach, markFilter)
1102
1103    def asFea(self, indent=""):
1104        res = []
1105        flags = ["RightToLeft", "IgnoreBaseGlyphs", "IgnoreLigatures", "IgnoreMarks"]
1106        curr = 1
1107        for i in range(len(flags)):
1108            if self.value & curr != 0:
1109                res.append(flags[i])
1110            curr = curr << 1
1111        if self.markAttachment is not None:
1112            res.append("MarkAttachmentType {}".format(self.markAttachment.asFea()))
1113        if self.markFilteringSet is not None:
1114            res.append("UseMarkFilteringSet {}".format(self.markFilteringSet.asFea()))
1115        if not res:
1116            res = ["0"]
1117        return "lookupflag {};".format(" ".join(res))
1118
1119
1120class LookupReferenceStatement(Statement):
1121    """Represents a ``lookup ...;`` statement to include a lookup in a feature.
1122
1123    The ``lookup`` should be a :class:`LookupBlock` object."""
1124
1125    def __init__(self, lookup, location=None):
1126        Statement.__init__(self, location)
1127        self.location, self.lookup = (location, lookup)
1128
1129    def build(self, builder):
1130        """Calls the builder object's ``add_lookup_call`` callback."""
1131        builder.add_lookup_call(self.lookup.name)
1132
1133    def asFea(self, indent=""):
1134        return "lookup {};".format(self.lookup.name)
1135
1136
1137class MarkBasePosStatement(Statement):
1138    """A mark-to-base positioning rule. The ``base`` should be a
1139    `glyph-containing object`_. The ``marks`` should be a list of
1140    (:class:`Anchor`, :class:`MarkClass`) tuples."""
1141
1142    def __init__(self, base, marks, location=None):
1143        Statement.__init__(self, location)
1144        self.base, self.marks = base, marks
1145
1146    def build(self, builder):
1147        """Calls the builder object's ``add_mark_base_pos`` callback."""
1148        builder.add_mark_base_pos(self.location, self.base.glyphSet(), self.marks)
1149
1150    def asFea(self, indent=""):
1151        res = "pos base {}".format(self.base.asFea())
1152        for a, m in self.marks:
1153            res += "\n" + indent + SHIFT + "{} mark @{}".format(a.asFea(), m.name)
1154        res += ";"
1155        return res
1156
1157
1158class MarkLigPosStatement(Statement):
1159    """A mark-to-ligature positioning rule. The ``ligatures`` must be a
1160    `glyph-containing object`_. The ``marks`` should be a list of lists: each
1161    element in the top-level list represents a component glyph, and is made
1162    up of a list of (:class:`Anchor`, :class:`MarkClass`) tuples representing
1163    mark attachment points for that position.
1164
1165    Example::
1166
1167        m1 = MarkClass("TOP_MARKS")
1168        m2 = MarkClass("BOTTOM_MARKS")
1169        # ... add definitions to mark classes...
1170
1171        glyph = GlyphName("lam_meem_jeem")
1172        marks = [
1173            [ (Anchor(625,1800), m1) ], # Attachments on 1st component (lam)
1174            [ (Anchor(376,-378), m2) ], # Attachments on 2nd component (meem)
1175            [ ]                         # No attachments on the jeem
1176        ]
1177        mlp = MarkLigPosStatement(glyph, marks)
1178
1179        mlp.asFea()
1180        # pos ligature lam_meem_jeem <anchor 625 1800> mark @TOP_MARKS
1181        # ligComponent <anchor 376 -378> mark @BOTTOM_MARKS;
1182
1183    """
1184
1185    def __init__(self, ligatures, marks, location=None):
1186        Statement.__init__(self, location)
1187        self.ligatures, self.marks = ligatures, marks
1188
1189    def build(self, builder):
1190        """Calls the builder object's ``add_mark_lig_pos`` callback."""
1191        builder.add_mark_lig_pos(self.location, self.ligatures.glyphSet(), self.marks)
1192
1193    def asFea(self, indent=""):
1194        res = "pos ligature {}".format(self.ligatures.asFea())
1195        ligs = []
1196        for l in self.marks:
1197            temp = ""
1198            if l is None or not len(l):
1199                temp = "\n" + indent + SHIFT * 2 + "<anchor NULL>"
1200            else:
1201                for a, m in l:
1202                    temp += (
1203                        "\n"
1204                        + indent
1205                        + SHIFT * 2
1206                        + "{} mark @{}".format(a.asFea(), m.name)
1207                    )
1208            ligs.append(temp)
1209        res += ("\n" + indent + SHIFT + "ligComponent").join(ligs)
1210        res += ";"
1211        return res
1212
1213
1214class MarkMarkPosStatement(Statement):
1215    """A mark-to-mark positioning rule. The ``baseMarks`` must be a
1216    `glyph-containing object`_. The ``marks`` should be a list of
1217    (:class:`Anchor`, :class:`MarkClass`) tuples."""
1218
1219    def __init__(self, baseMarks, marks, location=None):
1220        Statement.__init__(self, location)
1221        self.baseMarks, self.marks = baseMarks, marks
1222
1223    def build(self, builder):
1224        """Calls the builder object's ``add_mark_mark_pos`` callback."""
1225        builder.add_mark_mark_pos(self.location, self.baseMarks.glyphSet(), self.marks)
1226
1227    def asFea(self, indent=""):
1228        res = "pos mark {}".format(self.baseMarks.asFea())
1229        for a, m in self.marks:
1230            res += "\n" + indent + SHIFT + "{} mark @{}".format(a.asFea(), m.name)
1231        res += ";"
1232        return res
1233
1234
1235class MultipleSubstStatement(Statement):
1236    """A multiple substitution statement.
1237
1238    Args:
1239        prefix: a list of `glyph-containing objects`_.
1240        glyph: a single glyph-containing object.
1241        suffix: a list of glyph-containing objects.
1242        replacement: a list of glyph-containing objects.
1243        forceChain: If true, the statement is expressed as a chaining rule
1244            (e.g. ``sub f' i' by f_i``) even when no context is given.
1245    """
1246
1247    def __init__(
1248        self, prefix, glyph, suffix, replacement, forceChain=False, location=None
1249    ):
1250        Statement.__init__(self, location)
1251        self.prefix, self.glyph, self.suffix = prefix, glyph, suffix
1252        self.replacement = replacement
1253        self.forceChain = forceChain
1254
1255    def build(self, builder):
1256        """Calls the builder object's ``add_multiple_subst`` callback."""
1257        prefix = [p.glyphSet() for p in self.prefix]
1258        suffix = [s.glyphSet() for s in self.suffix]
1259        if hasattr(self.glyph, "glyphSet"):
1260            originals = self.glyph.glyphSet()
1261        else:
1262            originals = [self.glyph]
1263        count = len(originals)
1264        replaces = []
1265        for r in self.replacement:
1266            if hasattr(r, "glyphSet"):
1267                replace = r.glyphSet()
1268            else:
1269                replace = [r]
1270            if len(replace) == 1 and len(replace) != count:
1271                replace = replace * count
1272            replaces.append(replace)
1273        replaces = list(zip(*replaces))
1274
1275        seen_originals = set()
1276        for i, original in enumerate(originals):
1277            if original not in seen_originals:
1278                seen_originals.add(original)
1279                builder.add_multiple_subst(
1280                    self.location,
1281                    prefix,
1282                    original,
1283                    suffix,
1284                    replaces and replaces[i] or (),
1285                    self.forceChain,
1286                )
1287
1288    def asFea(self, indent=""):
1289        res = "sub "
1290        if len(self.prefix) or len(self.suffix) or self.forceChain:
1291            if len(self.prefix):
1292                res += " ".join(map(asFea, self.prefix)) + " "
1293            res += asFea(self.glyph) + "'"
1294            if len(self.suffix):
1295                res += " " + " ".join(map(asFea, self.suffix))
1296        else:
1297            res += asFea(self.glyph)
1298        replacement = self.replacement or [NullGlyph()]
1299        res += " by "
1300        res += " ".join(map(asFea, replacement))
1301        res += ";"
1302        return res
1303
1304
1305class PairPosStatement(Statement):
1306    """A pair positioning statement.
1307
1308    ``glyphs1`` and ``glyphs2`` should be `glyph-containing objects`_.
1309    ``valuerecord1`` should be a :class:`ValueRecord` object;
1310    ``valuerecord2`` should be either a :class:`ValueRecord` object or ``None``.
1311    If ``enumerated`` is true, then this is expressed as an
1312    `enumerated pair <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#6.b.ii>`_.
1313    """
1314
1315    def __init__(
1316        self,
1317        glyphs1,
1318        valuerecord1,
1319        glyphs2,
1320        valuerecord2,
1321        enumerated=False,
1322        location=None,
1323    ):
1324        Statement.__init__(self, location)
1325        self.enumerated = enumerated
1326        self.glyphs1, self.valuerecord1 = glyphs1, valuerecord1
1327        self.glyphs2, self.valuerecord2 = glyphs2, valuerecord2
1328
1329    def build(self, builder):
1330        """Calls a callback on the builder object:
1331
1332        * If the rule is enumerated, calls ``add_specific_pair_pos`` on each
1333          combination of first and second glyphs.
1334        * If the glyphs are both single :class:`GlyphName` objects, calls
1335          ``add_specific_pair_pos``.
1336        * Else, calls ``add_class_pair_pos``.
1337        """
1338        if self.enumerated:
1339            g = [self.glyphs1.glyphSet(), self.glyphs2.glyphSet()]
1340            seen_pair = False
1341            for glyph1, glyph2 in itertools.product(*g):
1342                seen_pair = True
1343                builder.add_specific_pair_pos(
1344                    self.location, glyph1, self.valuerecord1, glyph2, self.valuerecord2
1345                )
1346            if not seen_pair:
1347                raise FeatureLibError(
1348                    "Empty glyph class in positioning rule", self.location
1349                )
1350            return
1351
1352        is_specific = isinstance(self.glyphs1, GlyphName) and isinstance(
1353            self.glyphs2, GlyphName
1354        )
1355        if is_specific:
1356            builder.add_specific_pair_pos(
1357                self.location,
1358                self.glyphs1.glyph,
1359                self.valuerecord1,
1360                self.glyphs2.glyph,
1361                self.valuerecord2,
1362            )
1363        else:
1364            builder.add_class_pair_pos(
1365                self.location,
1366                self.glyphs1.glyphSet(),
1367                self.valuerecord1,
1368                self.glyphs2.glyphSet(),
1369                self.valuerecord2,
1370            )
1371
1372    def asFea(self, indent=""):
1373        res = "enum " if self.enumerated else ""
1374        if self.valuerecord2:
1375            res += "pos {} {} {} {};".format(
1376                self.glyphs1.asFea(),
1377                self.valuerecord1.asFea(),
1378                self.glyphs2.asFea(),
1379                self.valuerecord2.asFea(),
1380            )
1381        else:
1382            res += "pos {} {} {};".format(
1383                self.glyphs1.asFea(), self.glyphs2.asFea(), self.valuerecord1.asFea()
1384            )
1385        return res
1386
1387
1388class ReverseChainSingleSubstStatement(Statement):
1389    """A reverse chaining substitution statement. You don't see those every day.
1390
1391    Note the unusual argument order: ``suffix`` comes `before` ``glyphs``.
1392    ``old_prefix``, ``old_suffix``, ``glyphs`` and ``replacements`` should be
1393    lists of `glyph-containing objects`_. ``glyphs`` and ``replacements`` should
1394    be one-item lists.
1395    """
1396
1397    def __init__(self, old_prefix, old_suffix, glyphs, replacements, location=None):
1398        Statement.__init__(self, location)
1399        self.old_prefix, self.old_suffix = old_prefix, old_suffix
1400        self.glyphs = glyphs
1401        self.replacements = replacements
1402
1403    def build(self, builder):
1404        prefix = [p.glyphSet() for p in self.old_prefix]
1405        suffix = [s.glyphSet() for s in self.old_suffix]
1406        originals = self.glyphs[0].glyphSet()
1407        replaces = self.replacements[0].glyphSet()
1408        if len(replaces) == 1:
1409            replaces = replaces * len(originals)
1410        builder.add_reverse_chain_single_subst(
1411            self.location, prefix, suffix, dict(zip(originals, replaces))
1412        )
1413
1414    def asFea(self, indent=""):
1415        res = "rsub "
1416        if len(self.old_prefix) or len(self.old_suffix):
1417            if len(self.old_prefix):
1418                res += " ".join(asFea(g) for g in self.old_prefix) + " "
1419            res += " ".join(asFea(g) + "'" for g in self.glyphs)
1420            if len(self.old_suffix):
1421                res += " " + " ".join(asFea(g) for g in self.old_suffix)
1422        else:
1423            res += " ".join(map(asFea, self.glyphs))
1424        res += " by {};".format(" ".join(asFea(g) for g in self.replacements))
1425        return res
1426
1427
1428class SingleSubstStatement(Statement):
1429    """A single substitution statement.
1430
1431    Note the unusual argument order: ``prefix`` and suffix come `after`
1432    the replacement ``glyphs``. ``prefix``, ``suffix``, ``glyphs`` and
1433    ``replace`` should be lists of `glyph-containing objects`_. ``glyphs`` and
1434    ``replace`` should be one-item lists.
1435    """
1436
1437    def __init__(self, glyphs, replace, prefix, suffix, forceChain, location=None):
1438        Statement.__init__(self, location)
1439        self.prefix, self.suffix = prefix, suffix
1440        self.forceChain = forceChain
1441        self.glyphs = glyphs
1442        self.replacements = replace
1443
1444    def build(self, builder):
1445        """Calls the builder object's ``add_single_subst`` callback."""
1446        prefix = [p.glyphSet() for p in self.prefix]
1447        suffix = [s.glyphSet() for s in self.suffix]
1448        originals = self.glyphs[0].glyphSet()
1449        replaces = self.replacements[0].glyphSet()
1450        if len(replaces) == 1:
1451            replaces = replaces * len(originals)
1452        builder.add_single_subst(
1453            self.location,
1454            prefix,
1455            suffix,
1456            OrderedDict(zip(originals, replaces)),
1457            self.forceChain,
1458        )
1459
1460    def asFea(self, indent=""):
1461        res = "sub "
1462        if len(self.prefix) or len(self.suffix) or self.forceChain:
1463            if len(self.prefix):
1464                res += " ".join(asFea(g) for g in self.prefix) + " "
1465            res += " ".join(asFea(g) + "'" for g in self.glyphs)
1466            if len(self.suffix):
1467                res += " " + " ".join(asFea(g) for g in self.suffix)
1468        else:
1469            res += " ".join(asFea(g) for g in self.glyphs)
1470        res += " by {};".format(" ".join(asFea(g) for g in self.replacements))
1471        return res
1472
1473
1474class ScriptStatement(Statement):
1475    """A ``script`` statement."""
1476
1477    def __init__(self, script, location=None):
1478        Statement.__init__(self, location)
1479        self.script = script  #: the script code
1480
1481    def build(self, builder):
1482        """Calls the builder's ``set_script`` callback."""
1483        builder.set_script(self.location, self.script)
1484
1485    def asFea(self, indent=""):
1486        return "script {};".format(self.script.strip())
1487
1488
1489class SinglePosStatement(Statement):
1490    """A single position statement. ``prefix`` and ``suffix`` should be
1491    lists of `glyph-containing objects`_.
1492
1493    ``pos`` should be a one-element list containing a (`glyph-containing object`_,
1494    :class:`ValueRecord`) tuple."""
1495
1496    def __init__(self, pos, prefix, suffix, forceChain, location=None):
1497        Statement.__init__(self, location)
1498        self.pos, self.prefix, self.suffix = pos, prefix, suffix
1499        self.forceChain = forceChain
1500
1501    def build(self, builder):
1502        """Calls the builder object's ``add_single_pos`` callback."""
1503        prefix = [p.glyphSet() for p in self.prefix]
1504        suffix = [s.glyphSet() for s in self.suffix]
1505        pos = [(g.glyphSet(), value) for g, value in self.pos]
1506        builder.add_single_pos(self.location, prefix, suffix, pos, self.forceChain)
1507
1508    def asFea(self, indent=""):
1509        res = "pos "
1510        if len(self.prefix) or len(self.suffix) or self.forceChain:
1511            if len(self.prefix):
1512                res += " ".join(map(asFea, self.prefix)) + " "
1513            res += " ".join(
1514                [
1515                    asFea(x[0]) + "'" + ((" " + x[1].asFea()) if x[1] else "")
1516                    for x in self.pos
1517                ]
1518            )
1519            if len(self.suffix):
1520                res += " " + " ".join(map(asFea, self.suffix))
1521        else:
1522            res += " ".join(
1523                [asFea(x[0]) + " " + (x[1].asFea() if x[1] else "") for x in self.pos]
1524            )
1525        res += ";"
1526        return res
1527
1528
1529class SubtableStatement(Statement):
1530    """Represents a subtable break."""
1531
1532    def __init__(self, location=None):
1533        Statement.__init__(self, location)
1534
1535    def build(self, builder):
1536        """Calls the builder objects's ``add_subtable_break`` callback."""
1537        builder.add_subtable_break(self.location)
1538
1539    def asFea(self, indent=""):
1540        return "subtable;"
1541
1542
1543class ValueRecord(Expression):
1544    """Represents a value record."""
1545
1546    def __init__(
1547        self,
1548        xPlacement=None,
1549        yPlacement=None,
1550        xAdvance=None,
1551        yAdvance=None,
1552        xPlaDevice=None,
1553        yPlaDevice=None,
1554        xAdvDevice=None,
1555        yAdvDevice=None,
1556        vertical=False,
1557        location=None,
1558    ):
1559        Expression.__init__(self, location)
1560        self.xPlacement, self.yPlacement = (xPlacement, yPlacement)
1561        self.xAdvance, self.yAdvance = (xAdvance, yAdvance)
1562        self.xPlaDevice, self.yPlaDevice = (xPlaDevice, yPlaDevice)
1563        self.xAdvDevice, self.yAdvDevice = (xAdvDevice, yAdvDevice)
1564        self.vertical = vertical
1565
1566    def __eq__(self, other):
1567        return (
1568            self.xPlacement == other.xPlacement
1569            and self.yPlacement == other.yPlacement
1570            and self.xAdvance == other.xAdvance
1571            and self.yAdvance == other.yAdvance
1572            and self.xPlaDevice == other.xPlaDevice
1573            and self.xAdvDevice == other.xAdvDevice
1574        )
1575
1576    def __ne__(self, other):
1577        return not self.__eq__(other)
1578
1579    def __hash__(self):
1580        return (
1581            hash(self.xPlacement)
1582            ^ hash(self.yPlacement)
1583            ^ hash(self.xAdvance)
1584            ^ hash(self.yAdvance)
1585            ^ hash(self.xPlaDevice)
1586            ^ hash(self.yPlaDevice)
1587            ^ hash(self.xAdvDevice)
1588            ^ hash(self.yAdvDevice)
1589        )
1590
1591    def asFea(self, indent=""):
1592        if not self:
1593            return "<NULL>"
1594
1595        x, y = self.xPlacement, self.yPlacement
1596        xAdvance, yAdvance = self.xAdvance, self.yAdvance
1597        xPlaDevice, yPlaDevice = self.xPlaDevice, self.yPlaDevice
1598        xAdvDevice, yAdvDevice = self.xAdvDevice, self.yAdvDevice
1599        vertical = self.vertical
1600
1601        # Try format A, if possible.
1602        if x is None and y is None:
1603            if xAdvance is None and vertical:
1604                return str(yAdvance)
1605            elif yAdvance is None and not vertical:
1606                return str(xAdvance)
1607
1608        # Make any remaining None value 0 to avoid generating invalid records.
1609        x = x or 0
1610        y = y or 0
1611        xAdvance = xAdvance or 0
1612        yAdvance = yAdvance or 0
1613
1614        # Try format B, if possible.
1615        if (
1616            xPlaDevice is None
1617            and yPlaDevice is None
1618            and xAdvDevice is None
1619            and yAdvDevice is None
1620        ):
1621            return "<%s %s %s %s>" % (x, y, xAdvance, yAdvance)
1622
1623        # Last resort is format C.
1624        return "<%s %s %s %s %s %s %s %s>" % (
1625            x,
1626            y,
1627            xAdvance,
1628            yAdvance,
1629            deviceToString(xPlaDevice),
1630            deviceToString(yPlaDevice),
1631            deviceToString(xAdvDevice),
1632            deviceToString(yAdvDevice),
1633        )
1634
1635    def __bool__(self):
1636        return any(
1637            getattr(self, v) is not None
1638            for v in [
1639                "xPlacement",
1640                "yPlacement",
1641                "xAdvance",
1642                "yAdvance",
1643                "xPlaDevice",
1644                "yPlaDevice",
1645                "xAdvDevice",
1646                "yAdvDevice",
1647            ]
1648        )
1649
1650    __nonzero__ = __bool__
1651
1652
1653class ValueRecordDefinition(Statement):
1654    """Represents a named value record definition."""
1655
1656    def __init__(self, name, value, location=None):
1657        Statement.__init__(self, location)
1658        self.name = name  #: Value record name as string
1659        self.value = value  #: :class:`ValueRecord` object
1660
1661    def asFea(self, indent=""):
1662        return "valueRecordDef {} {};".format(self.value.asFea(), self.name)
1663
1664
1665def simplify_name_attributes(pid, eid, lid):
1666    if pid == 3 and eid == 1 and lid == 1033:
1667        return ""
1668    elif pid == 1 and eid == 0 and lid == 0:
1669        return "1"
1670    else:
1671        return "{} {} {}".format(pid, eid, lid)
1672
1673
1674class NameRecord(Statement):
1675    """Represents a name record. (`Section 9.e. <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.e>`_)"""
1676
1677    def __init__(self, nameID, platformID, platEncID, langID, string, location=None):
1678        Statement.__init__(self, location)
1679        self.nameID = nameID  #: Name ID as integer (e.g. 9 for designer's name)
1680        self.platformID = platformID  #: Platform ID as integer
1681        self.platEncID = platEncID  #: Platform encoding ID as integer
1682        self.langID = langID  #: Language ID as integer
1683        self.string = string  #: Name record value
1684
1685    def build(self, builder):
1686        """Calls the builder object's ``add_name_record`` callback."""
1687        builder.add_name_record(
1688            self.location,
1689            self.nameID,
1690            self.platformID,
1691            self.platEncID,
1692            self.langID,
1693            self.string,
1694        )
1695
1696    def asFea(self, indent=""):
1697        def escape(c, escape_pattern):
1698            # Also escape U+0022 QUOTATION MARK and U+005C REVERSE SOLIDUS
1699            if c >= 0x20 and c <= 0x7E and c not in (0x22, 0x5C):
1700                return chr(c)
1701            else:
1702                return escape_pattern % c
1703
1704        encoding = getEncoding(self.platformID, self.platEncID, self.langID)
1705        if encoding is None:
1706            raise FeatureLibError("Unsupported encoding", self.location)
1707        s = tobytes(self.string, encoding=encoding)
1708        if encoding == "utf_16_be":
1709            escaped_string = "".join(
1710                [
1711                    escape(byteord(s[i]) * 256 + byteord(s[i + 1]), r"\%04x")
1712                    for i in range(0, len(s), 2)
1713                ]
1714            )
1715        else:
1716            escaped_string = "".join([escape(byteord(b), r"\%02x") for b in s])
1717        plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID)
1718        if plat != "":
1719            plat += " "
1720        return 'nameid {} {}"{}";'.format(self.nameID, plat, escaped_string)
1721
1722
1723class FeatureNameStatement(NameRecord):
1724    """Represents a ``sizemenuname`` or ``name`` statement."""
1725
1726    def build(self, builder):
1727        """Calls the builder object's ``add_featureName`` callback."""
1728        NameRecord.build(self, builder)
1729        builder.add_featureName(self.nameID)
1730
1731    def asFea(self, indent=""):
1732        if self.nameID == "size":
1733            tag = "sizemenuname"
1734        else:
1735            tag = "name"
1736        plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID)
1737        if plat != "":
1738            plat += " "
1739        return '{} {}"{}";'.format(tag, plat, self.string)
1740
1741
1742class STATNameStatement(NameRecord):
1743    """Represents a STAT table ``name`` statement."""
1744
1745    def asFea(self, indent=""):
1746        plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID)
1747        if plat != "":
1748            plat += " "
1749        return 'name {}"{}";'.format(plat, self.string)
1750
1751
1752class SizeParameters(Statement):
1753    """A ``parameters`` statement."""
1754
1755    def __init__(self, DesignSize, SubfamilyID, RangeStart, RangeEnd, location=None):
1756        Statement.__init__(self, location)
1757        self.DesignSize = DesignSize
1758        self.SubfamilyID = SubfamilyID
1759        self.RangeStart = RangeStart
1760        self.RangeEnd = RangeEnd
1761
1762    def build(self, builder):
1763        """Calls the builder object's ``set_size_parameters`` callback."""
1764        builder.set_size_parameters(
1765            self.location,
1766            self.DesignSize,
1767            self.SubfamilyID,
1768            self.RangeStart,
1769            self.RangeEnd,
1770        )
1771
1772    def asFea(self, indent=""):
1773        res = "parameters {:.1f} {}".format(self.DesignSize, self.SubfamilyID)
1774        if self.RangeStart != 0 or self.RangeEnd != 0:
1775            res += " {} {}".format(int(self.RangeStart * 10), int(self.RangeEnd * 10))
1776        return res + ";"
1777
1778
1779class CVParametersNameStatement(NameRecord):
1780    """Represent a name statement inside a ``cvParameters`` block."""
1781
1782    def __init__(
1783        self, nameID, platformID, platEncID, langID, string, block_name, location=None
1784    ):
1785        NameRecord.__init__(
1786            self, nameID, platformID, platEncID, langID, string, location=location
1787        )
1788        self.block_name = block_name
1789
1790    def build(self, builder):
1791        """Calls the builder object's ``add_cv_parameter`` callback."""
1792        item = ""
1793        if self.block_name == "ParamUILabelNameID":
1794            item = "_{}".format(builder.cv_num_named_params_.get(self.nameID, 0))
1795        builder.add_cv_parameter(self.nameID)
1796        self.nameID = (self.nameID, self.block_name + item)
1797        NameRecord.build(self, builder)
1798
1799    def asFea(self, indent=""):
1800        plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID)
1801        if plat != "":
1802            plat += " "
1803        return 'name {}"{}";'.format(plat, self.string)
1804
1805
1806class CharacterStatement(Statement):
1807    """
1808    Statement used in cvParameters blocks of Character Variant features (cvXX).
1809    The Unicode value may be written with either decimal or hexadecimal
1810    notation. The value must be preceded by '0x' if it is a hexadecimal value.
1811    The largest Unicode value allowed is 0xFFFFFF.
1812    """
1813
1814    def __init__(self, character, tag, location=None):
1815        Statement.__init__(self, location)
1816        self.character = character
1817        self.tag = tag
1818
1819    def build(self, builder):
1820        """Calls the builder object's ``add_cv_character`` callback."""
1821        builder.add_cv_character(self.character, self.tag)
1822
1823    def asFea(self, indent=""):
1824        return "Character {:#x};".format(self.character)
1825
1826
1827class BaseAxis(Statement):
1828    """An axis definition, being either a ``VertAxis.BaseTagList/BaseScriptList``
1829    pair or a ``HorizAxis.BaseTagList/BaseScriptList`` pair."""
1830
1831    def __init__(self, bases, scripts, vertical, location=None):
1832        Statement.__init__(self, location)
1833        self.bases = bases  #: A list of baseline tag names as strings
1834        self.scripts = scripts  #: A list of script record tuplets (script tag, default baseline tag, base coordinate)
1835        self.vertical = vertical  #: Boolean; VertAxis if True, HorizAxis if False
1836
1837    def build(self, builder):
1838        """Calls the builder object's ``set_base_axis`` callback."""
1839        builder.set_base_axis(self.bases, self.scripts, self.vertical)
1840
1841    def asFea(self, indent=""):
1842        direction = "Vert" if self.vertical else "Horiz"
1843        scripts = [
1844            "{} {} {}".format(a[0], a[1], " ".join(map(str, a[2])))
1845            for a in self.scripts
1846        ]
1847        return "{}Axis.BaseTagList {};\n{}{}Axis.BaseScriptList {};".format(
1848            direction, " ".join(self.bases), indent, direction, ", ".join(scripts)
1849        )
1850
1851
1852class OS2Field(Statement):
1853    """An entry in the ``OS/2`` table. Most ``values`` should be numbers or
1854    strings, apart from when the key is ``UnicodeRange``, ``CodePageRange``
1855    or ``Panose``, in which case it should be an array of integers."""
1856
1857    def __init__(self, key, value, location=None):
1858        Statement.__init__(self, location)
1859        self.key = key
1860        self.value = value
1861
1862    def build(self, builder):
1863        """Calls the builder object's ``add_os2_field`` callback."""
1864        builder.add_os2_field(self.key, self.value)
1865
1866    def asFea(self, indent=""):
1867        def intarr2str(x):
1868            return " ".join(map(str, x))
1869
1870        numbers = (
1871            "FSType",
1872            "TypoAscender",
1873            "TypoDescender",
1874            "TypoLineGap",
1875            "winAscent",
1876            "winDescent",
1877            "XHeight",
1878            "CapHeight",
1879            "WeightClass",
1880            "WidthClass",
1881            "LowerOpSize",
1882            "UpperOpSize",
1883        )
1884        ranges = ("UnicodeRange", "CodePageRange")
1885        keywords = dict([(x.lower(), [x, str]) for x in numbers])
1886        keywords.update([(x.lower(), [x, intarr2str]) for x in ranges])
1887        keywords["panose"] = ["Panose", intarr2str]
1888        keywords["vendor"] = ["Vendor", lambda y: '"{}"'.format(y)]
1889        if self.key in keywords:
1890            return "{} {};".format(
1891                keywords[self.key][0], keywords[self.key][1](self.value)
1892            )
1893        return ""  # should raise exception
1894
1895
1896class HheaField(Statement):
1897    """An entry in the ``hhea`` table."""
1898
1899    def __init__(self, key, value, location=None):
1900        Statement.__init__(self, location)
1901        self.key = key
1902        self.value = value
1903
1904    def build(self, builder):
1905        """Calls the builder object's ``add_hhea_field`` callback."""
1906        builder.add_hhea_field(self.key, self.value)
1907
1908    def asFea(self, indent=""):
1909        fields = ("CaretOffset", "Ascender", "Descender", "LineGap")
1910        keywords = dict([(x.lower(), x) for x in fields])
1911        return "{} {};".format(keywords[self.key], self.value)
1912
1913
1914class VheaField(Statement):
1915    """An entry in the ``vhea`` table."""
1916
1917    def __init__(self, key, value, location=None):
1918        Statement.__init__(self, location)
1919        self.key = key
1920        self.value = value
1921
1922    def build(self, builder):
1923        """Calls the builder object's ``add_vhea_field`` callback."""
1924        builder.add_vhea_field(self.key, self.value)
1925
1926    def asFea(self, indent=""):
1927        fields = ("VertTypoAscender", "VertTypoDescender", "VertTypoLineGap")
1928        keywords = dict([(x.lower(), x) for x in fields])
1929        return "{} {};".format(keywords[self.key], self.value)
1930
1931
1932class STATDesignAxisStatement(Statement):
1933    """A STAT table Design Axis
1934
1935    Args:
1936        tag (str): a 4 letter axis tag
1937        axisOrder (int): an int
1938        names (list): a list of :class:`STATNameStatement` objects
1939    """
1940
1941    def __init__(self, tag, axisOrder, names, location=None):
1942        Statement.__init__(self, location)
1943        self.tag = tag
1944        self.axisOrder = axisOrder
1945        self.names = names
1946        self.location = location
1947
1948    def build(self, builder):
1949        builder.addDesignAxis(self, self.location)
1950
1951    def asFea(self, indent=""):
1952        indent += SHIFT
1953        res = f"DesignAxis {self.tag} {self.axisOrder} {{ \n"
1954        res += ("\n" + indent).join([s.asFea(indent=indent) for s in self.names]) + "\n"
1955        res += "};"
1956        return res
1957
1958
1959class ElidedFallbackName(Statement):
1960    """STAT table ElidedFallbackName
1961
1962    Args:
1963        names: a list of :class:`STATNameStatement` objects
1964    """
1965
1966    def __init__(self, names, location=None):
1967        Statement.__init__(self, location)
1968        self.names = names
1969        self.location = location
1970
1971    def build(self, builder):
1972        builder.setElidedFallbackName(self.names, self.location)
1973
1974    def asFea(self, indent=""):
1975        indent += SHIFT
1976        res = "ElidedFallbackName { \n"
1977        res += ("\n" + indent).join([s.asFea(indent=indent) for s in self.names]) + "\n"
1978        res += "};"
1979        return res
1980
1981
1982class ElidedFallbackNameID(Statement):
1983    """STAT table ElidedFallbackNameID
1984
1985    Args:
1986        value: an int pointing to an existing name table name ID
1987    """
1988
1989    def __init__(self, value, location=None):
1990        Statement.__init__(self, location)
1991        self.value = value
1992        self.location = location
1993
1994    def build(self, builder):
1995        builder.setElidedFallbackName(self.value, self.location)
1996
1997    def asFea(self, indent=""):
1998        return f"ElidedFallbackNameID {self.value};"
1999
2000
2001class STATAxisValueStatement(Statement):
2002    """A STAT table Axis Value Record
2003
2004    Args:
2005        names (list): a list of :class:`STATNameStatement` objects
2006        locations (list): a list of :class:`AxisValueLocationStatement` objects
2007        flags (int): an int
2008    """
2009
2010    def __init__(self, names, locations, flags, location=None):
2011        Statement.__init__(self, location)
2012        self.names = names
2013        self.locations = locations
2014        self.flags = flags
2015
2016    def build(self, builder):
2017        builder.addAxisValueRecord(self, self.location)
2018
2019    def asFea(self, indent=""):
2020        res = "AxisValue {\n"
2021        for location in self.locations:
2022            res += location.asFea()
2023
2024        for nameRecord in self.names:
2025            res += nameRecord.asFea()
2026            res += "\n"
2027
2028        if self.flags:
2029            flags = ["OlderSiblingFontAttribute", "ElidableAxisValueName"]
2030            flagStrings = []
2031            curr = 1
2032            for i in range(len(flags)):
2033                if self.flags & curr != 0:
2034                    flagStrings.append(flags[i])
2035                curr = curr << 1
2036            res += f"flag {' '.join(flagStrings)};\n"
2037        res += "};"
2038        return res
2039
2040
2041class AxisValueLocationStatement(Statement):
2042    """
2043    A STAT table Axis Value Location
2044
2045    Args:
2046        tag (str): a 4 letter axis tag
2047        values (list): a list of ints and/or floats
2048    """
2049
2050    def __init__(self, tag, values, location=None):
2051        Statement.__init__(self, location)
2052        self.tag = tag
2053        self.values = values
2054
2055    def asFea(self, res=""):
2056        res += f"location {self.tag} "
2057        res += f"{' '.join(str(i) for i in self.values)};\n"
2058        return res
2059
2060
2061class ConditionsetStatement(Statement):
2062    """
2063    A variable layout conditionset
2064
2065    Args:
2066        name (str): the name of this conditionset
2067        conditions (dict): a dictionary mapping axis tags to a
2068            tuple of (min,max) userspace coordinates.
2069    """
2070
2071    def __init__(self, name, conditions, location=None):
2072        Statement.__init__(self, location)
2073        self.name = name
2074        self.conditions = conditions
2075
2076    def build(self, builder):
2077        builder.add_conditionset(self.location, self.name, self.conditions)
2078
2079    def asFea(self, res="", indent=""):
2080        res += indent + f"conditionset {self.name} " + "{\n"
2081        for tag, (minvalue, maxvalue) in self.conditions.items():
2082            res += indent + SHIFT + f"{tag} {minvalue} {maxvalue};\n"
2083        res += indent + "}" + f" {self.name};\n"
2084        return res
2085
2086
2087class VariationBlock(Block):
2088    """A variation feature block, applicable in a given set of conditions."""
2089
2090    def __init__(self, name, conditionset, use_extension=False, location=None):
2091        Block.__init__(self, location)
2092        self.name, self.conditionset, self.use_extension = (
2093            name,
2094            conditionset,
2095            use_extension,
2096        )
2097
2098    def build(self, builder):
2099        """Call the ``start_feature`` callback on the builder object, visit
2100        all the statements in this feature, and then call ``end_feature``."""
2101        builder.start_feature(self.location, self.name)
2102        if (
2103            self.conditionset != "NULL"
2104            and self.conditionset not in builder.conditionsets_
2105        ):
2106            raise FeatureLibError(
2107                f"variation block used undefined conditionset {self.conditionset}",
2108                self.location,
2109            )
2110
2111        # language exclude_dflt statements modify builder.features_
2112        # limit them to this block with temporary builder.features_
2113        features = builder.features_
2114        builder.features_ = {}
2115        Block.build(self, builder)
2116        for key, value in builder.features_.items():
2117            items = builder.feature_variations_.setdefault(key, {}).setdefault(
2118                self.conditionset, []
2119            )
2120            items.extend(value)
2121            if key not in features:
2122                features[key] = []  # Ensure we make a feature record
2123        builder.features_ = features
2124        builder.end_feature()
2125
2126    def asFea(self, indent=""):
2127        res = indent + "variation %s " % self.name.strip()
2128        res += self.conditionset + " "
2129        if self.use_extension:
2130            res += "useExtension "
2131        res += "{\n"
2132        res += Block.asFea(self, indent=indent)
2133        res += indent + "} %s;\n" % self.name.strip()
2134        return res
2135