• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from fontTools.feaLib.error import FeatureLibError
2from fontTools.feaLib.lexer import Lexer, IncludingLexer, NonIncludingLexer
3from fontTools.misc.encodingTools import getEncoding
4from fontTools.misc.py23 import bytechr, tobytes, tostr
5import fontTools.feaLib.ast as ast
6import logging
7import os
8import re
9
10
11log = logging.getLogger(__name__)
12
13
14class Parser(object):
15    """Initializes a Parser object.
16
17    Example:
18
19        .. code:: python
20
21            from fontTools.feaLib.parser import Parser
22            parser = Parser(file, font.getReverseGlyphMap())
23            parsetree = parser.parse()
24
25    Note: the ``glyphNames`` iterable serves a double role to help distinguish
26    glyph names from ranges in the presence of hyphens and to ensure that glyph
27    names referenced in a feature file are actually part of a font's glyph set.
28    If the iterable is left empty, no glyph name in glyph set checking takes
29    place, and all glyph tokens containing hyphens are treated as literal glyph
30    names, not as ranges. (Adding a space around the hyphen can, in any case,
31    help to disambiguate ranges from glyph names containing hyphens.)
32
33    By default, the parser will follow ``include()`` statements in the feature
34    file. To turn this off, pass ``followIncludes=False``. Pass a directory string as
35    ``includeDir`` to explicitly declare a directory to search included feature files
36    in.
37    """
38
39    extensions = {}
40    ast = ast
41    SS_FEATURE_TAGS = {"ss%02d" % i for i in range(1, 20 + 1)}
42    CV_FEATURE_TAGS = {"cv%02d" % i for i in range(1, 99 + 1)}
43
44    def __init__(
45        self, featurefile, glyphNames=(), followIncludes=True, includeDir=None, **kwargs
46    ):
47
48        if "glyphMap" in kwargs:
49            from fontTools.misc.loggingTools import deprecateArgument
50
51            deprecateArgument("glyphMap", "use 'glyphNames' (iterable) instead")
52            if glyphNames:
53                raise TypeError(
54                    "'glyphNames' and (deprecated) 'glyphMap' are " "mutually exclusive"
55                )
56            glyphNames = kwargs.pop("glyphMap")
57        if kwargs:
58            raise TypeError(
59                "unsupported keyword argument%s: %s"
60                % ("" if len(kwargs) == 1 else "s", ", ".join(repr(k) for k in kwargs))
61            )
62
63        self.glyphNames_ = set(glyphNames)
64        self.doc_ = self.ast.FeatureFile()
65        self.anchors_ = SymbolTable()
66        self.glyphclasses_ = SymbolTable()
67        self.lookups_ = SymbolTable()
68        self.valuerecords_ = SymbolTable()
69        self.symbol_tables_ = {self.anchors_, self.valuerecords_}
70        self.next_token_type_, self.next_token_ = (None, None)
71        self.cur_comments_ = []
72        self.next_token_location_ = None
73        lexerClass = IncludingLexer if followIncludes else NonIncludingLexer
74        self.lexer_ = lexerClass(featurefile, includeDir=includeDir)
75        self.advance_lexer_(comments=True)
76
77    def parse(self):
78        """Parse the file, and return a :class:`fontTools.feaLib.ast.FeatureFile`
79        object representing the root of the abstract syntax tree containing the
80        parsed contents of the file."""
81        statements = self.doc_.statements
82        while self.next_token_type_ is not None or self.cur_comments_:
83            self.advance_lexer_(comments=True)
84            if self.cur_token_type_ is Lexer.COMMENT:
85                statements.append(
86                    self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
87                )
88            elif self.is_cur_keyword_("include"):
89                statements.append(self.parse_include_())
90            elif self.cur_token_type_ is Lexer.GLYPHCLASS:
91                statements.append(self.parse_glyphclass_definition_())
92            elif self.is_cur_keyword_(("anon", "anonymous")):
93                statements.append(self.parse_anonymous_())
94            elif self.is_cur_keyword_("anchorDef"):
95                statements.append(self.parse_anchordef_())
96            elif self.is_cur_keyword_("languagesystem"):
97                statements.append(self.parse_languagesystem_())
98            elif self.is_cur_keyword_("lookup"):
99                statements.append(self.parse_lookup_(vertical=False))
100            elif self.is_cur_keyword_("markClass"):
101                statements.append(self.parse_markClass_())
102            elif self.is_cur_keyword_("feature"):
103                statements.append(self.parse_feature_block_())
104            elif self.is_cur_keyword_("table"):
105                statements.append(self.parse_table_())
106            elif self.is_cur_keyword_("valueRecordDef"):
107                statements.append(self.parse_valuerecord_definition_(vertical=False))
108            elif (
109                self.cur_token_type_ is Lexer.NAME
110                and self.cur_token_ in self.extensions
111            ):
112                statements.append(self.extensions[self.cur_token_](self))
113            elif self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == ";":
114                continue
115            else:
116                raise FeatureLibError(
117                    "Expected feature, languagesystem, lookup, markClass, "
118                    'table, or glyph class definition, got {} "{}"'.format(
119                        self.cur_token_type_, self.cur_token_
120                    ),
121                    self.cur_token_location_,
122                )
123        return self.doc_
124
125    def parse_anchor_(self):
126        # Parses an anchor in any of the four formats given in the feature
127        # file specification (2.e.vii).
128        self.expect_symbol_("<")
129        self.expect_keyword_("anchor")
130        location = self.cur_token_location_
131
132        if self.next_token_ == "NULL":  # Format D
133            self.expect_keyword_("NULL")
134            self.expect_symbol_(">")
135            return None
136
137        if self.next_token_type_ == Lexer.NAME:  # Format E
138            name = self.expect_name_()
139            anchordef = self.anchors_.resolve(name)
140            if anchordef is None:
141                raise FeatureLibError(
142                    'Unknown anchor "%s"' % name, self.cur_token_location_
143                )
144            self.expect_symbol_(">")
145            return self.ast.Anchor(
146                anchordef.x,
147                anchordef.y,
148                name=name,
149                contourpoint=anchordef.contourpoint,
150                xDeviceTable=None,
151                yDeviceTable=None,
152                location=location,
153            )
154
155        x, y = self.expect_number_(), self.expect_number_()
156
157        contourpoint = None
158        if self.next_token_ == "contourpoint":  # Format B
159            self.expect_keyword_("contourpoint")
160            contourpoint = self.expect_number_()
161
162        if self.next_token_ == "<":  # Format C
163            xDeviceTable = self.parse_device_()
164            yDeviceTable = self.parse_device_()
165        else:
166            xDeviceTable, yDeviceTable = None, None
167
168        self.expect_symbol_(">")
169        return self.ast.Anchor(
170            x,
171            y,
172            name=None,
173            contourpoint=contourpoint,
174            xDeviceTable=xDeviceTable,
175            yDeviceTable=yDeviceTable,
176            location=location,
177        )
178
179    def parse_anchor_marks_(self):
180        # Parses a sequence of ``[<anchor> mark @MARKCLASS]*.``
181        anchorMarks = []  # [(self.ast.Anchor, markClassName)*]
182        while self.next_token_ == "<":
183            anchor = self.parse_anchor_()
184            if anchor is None and self.next_token_ != "mark":
185                continue  # <anchor NULL> without mark, eg. in GPOS type 5
186            self.expect_keyword_("mark")
187            markClass = self.expect_markClass_reference_()
188            anchorMarks.append((anchor, markClass))
189        return anchorMarks
190
191    def parse_anchordef_(self):
192        # Parses a named anchor definition (`section 2.e.viii <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#2.e.vii>`_).
193        assert self.is_cur_keyword_("anchorDef")
194        location = self.cur_token_location_
195        x, y = self.expect_number_(), self.expect_number_()
196        contourpoint = None
197        if self.next_token_ == "contourpoint":
198            self.expect_keyword_("contourpoint")
199            contourpoint = self.expect_number_()
200        name = self.expect_name_()
201        self.expect_symbol_(";")
202        anchordef = self.ast.AnchorDefinition(
203            name, x, y, contourpoint=contourpoint, location=location
204        )
205        self.anchors_.define(name, anchordef)
206        return anchordef
207
208    def parse_anonymous_(self):
209        # Parses an anonymous data block (`section 10 <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#10>`_).
210        assert self.is_cur_keyword_(("anon", "anonymous"))
211        tag = self.expect_tag_()
212        _, content, location = self.lexer_.scan_anonymous_block(tag)
213        self.advance_lexer_()
214        self.expect_symbol_("}")
215        end_tag = self.expect_tag_()
216        assert tag == end_tag, "bad splitting in Lexer.scan_anonymous_block()"
217        self.expect_symbol_(";")
218        return self.ast.AnonymousBlock(tag, content, location=location)
219
220    def parse_attach_(self):
221        # Parses a GDEF Attach statement (`section 9.b <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.b>`_)
222        assert self.is_cur_keyword_("Attach")
223        location = self.cur_token_location_
224        glyphs = self.parse_glyphclass_(accept_glyphname=True)
225        contourPoints = {self.expect_number_()}
226        while self.next_token_ != ";":
227            contourPoints.add(self.expect_number_())
228        self.expect_symbol_(";")
229        return self.ast.AttachStatement(glyphs, contourPoints, location=location)
230
231    def parse_enumerate_(self, vertical):
232        # Parse an enumerated pair positioning rule (`section 6.b.ii <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#6.b.ii>`_).
233        assert self.cur_token_ in {"enumerate", "enum"}
234        self.advance_lexer_()
235        return self.parse_position_(enumerated=True, vertical=vertical)
236
237    def parse_GlyphClassDef_(self):
238        # Parses 'GlyphClassDef @BASE, @LIGATURES, @MARKS, @COMPONENTS;'
239        assert self.is_cur_keyword_("GlyphClassDef")
240        location = self.cur_token_location_
241        if self.next_token_ != ",":
242            baseGlyphs = self.parse_glyphclass_(accept_glyphname=False)
243        else:
244            baseGlyphs = None
245        self.expect_symbol_(",")
246        if self.next_token_ != ",":
247            ligatureGlyphs = self.parse_glyphclass_(accept_glyphname=False)
248        else:
249            ligatureGlyphs = None
250        self.expect_symbol_(",")
251        if self.next_token_ != ",":
252            markGlyphs = self.parse_glyphclass_(accept_glyphname=False)
253        else:
254            markGlyphs = None
255        self.expect_symbol_(",")
256        if self.next_token_ != ";":
257            componentGlyphs = self.parse_glyphclass_(accept_glyphname=False)
258        else:
259            componentGlyphs = None
260        self.expect_symbol_(";")
261        return self.ast.GlyphClassDefStatement(
262            baseGlyphs, markGlyphs, ligatureGlyphs, componentGlyphs, location=location
263        )
264
265    def parse_glyphclass_definition_(self):
266        # Parses glyph class definitions such as '@UPPERCASE = [A-Z];'
267        location, name = self.cur_token_location_, self.cur_token_
268        self.expect_symbol_("=")
269        glyphs = self.parse_glyphclass_(accept_glyphname=False)
270        self.expect_symbol_(";")
271        glyphclass = self.ast.GlyphClassDefinition(name, glyphs, location=location)
272        self.glyphclasses_.define(name, glyphclass)
273        return glyphclass
274
275    def split_glyph_range_(self, name, location):
276        # Since v1.20, the OpenType Feature File specification allows
277        # for dashes in glyph names. A sequence like "a-b-c-d" could
278        # therefore mean a single glyph whose name happens to be
279        # "a-b-c-d", or it could mean a range from glyph "a" to glyph
280        # "b-c-d", or a range from glyph "a-b" to glyph "c-d", or a
281        # range from glyph "a-b-c" to glyph "d".Technically, this
282        # example could be resolved because the (pretty complex)
283        # definition of glyph ranges renders most of these splits
284        # invalid. But the specification does not say that a compiler
285        # should try to apply such fancy heuristics. To encourage
286        # unambiguous feature files, we therefore try all possible
287        # splits and reject the feature file if there are multiple
288        # splits possible. It is intentional that we don't just emit a
289        # warning; warnings tend to get ignored. To fix the problem,
290        # font designers can trivially add spaces around the intended
291        # split point, and we emit a compiler error that suggests
292        # how exactly the source should be rewritten to make things
293        # unambiguous.
294        parts = name.split("-")
295        solutions = []
296        for i in range(len(parts)):
297            start, limit = "-".join(parts[0:i]), "-".join(parts[i:])
298            if start in self.glyphNames_ and limit in self.glyphNames_:
299                solutions.append((start, limit))
300        if len(solutions) == 1:
301            start, limit = solutions[0]
302            return start, limit
303        elif len(solutions) == 0:
304            raise FeatureLibError(
305                '"%s" is not a glyph in the font, and it can not be split '
306                "into a range of known glyphs" % name,
307                location,
308            )
309        else:
310            ranges = " or ".join(['"%s - %s"' % (s, l) for s, l in solutions])
311            raise FeatureLibError(
312                'Ambiguous glyph range "%s"; '
313                "please use %s to clarify what you mean" % (name, ranges),
314                location,
315            )
316
317    def parse_glyphclass_(self, accept_glyphname, accept_null=False):
318        # Parses a glyph class, either named or anonymous, or (if
319        # ``bool(accept_glyphname)``) a glyph name. If ``bool(accept_null)`` then
320        # also accept the special NULL glyph.
321        if accept_glyphname and self.next_token_type_ in (Lexer.NAME, Lexer.CID):
322            if accept_null and self.next_token_ == "NULL":
323                # If you want a glyph called NULL, you should escape it.
324                self.advance_lexer_()
325                return self.ast.NullGlyph(location=self.cur_token_location_)
326            glyph = self.expect_glyph_()
327            self.check_glyph_name_in_glyph_set(glyph)
328            return self.ast.GlyphName(glyph, location=self.cur_token_location_)
329        if self.next_token_type_ is Lexer.GLYPHCLASS:
330            self.advance_lexer_()
331            gc = self.glyphclasses_.resolve(self.cur_token_)
332            if gc is None:
333                raise FeatureLibError(
334                    "Unknown glyph class @%s" % self.cur_token_,
335                    self.cur_token_location_,
336                )
337            if isinstance(gc, self.ast.MarkClass):
338                return self.ast.MarkClassName(gc, location=self.cur_token_location_)
339            else:
340                return self.ast.GlyphClassName(gc, location=self.cur_token_location_)
341
342        self.expect_symbol_("[")
343        location = self.cur_token_location_
344        glyphs = self.ast.GlyphClass(location=location)
345        while self.next_token_ != "]":
346            if self.next_token_type_ is Lexer.NAME:
347                glyph = self.expect_glyph_()
348                location = self.cur_token_location_
349                if "-" in glyph and self.glyphNames_ and glyph not in self.glyphNames_:
350                    start, limit = self.split_glyph_range_(glyph, location)
351                    self.check_glyph_name_in_glyph_set(start, limit)
352                    glyphs.add_range(
353                        start, limit, self.make_glyph_range_(location, start, limit)
354                    )
355                elif self.next_token_ == "-":
356                    start = glyph
357                    self.expect_symbol_("-")
358                    limit = self.expect_glyph_()
359                    self.check_glyph_name_in_glyph_set(start, limit)
360                    glyphs.add_range(
361                        start, limit, self.make_glyph_range_(location, start, limit)
362                    )
363                else:
364                    if "-" in glyph and not self.glyphNames_:
365                        log.warning(
366                            str(
367                                FeatureLibError(
368                                    f"Ambiguous glyph name that looks like a range: {glyph!r}",
369                                    location,
370                                )
371                            )
372                        )
373                    self.check_glyph_name_in_glyph_set(glyph)
374                    glyphs.append(glyph)
375            elif self.next_token_type_ is Lexer.CID:
376                glyph = self.expect_glyph_()
377                if self.next_token_ == "-":
378                    range_location = self.cur_token_location_
379                    range_start = self.cur_token_
380                    self.expect_symbol_("-")
381                    range_end = self.expect_cid_()
382                    self.check_glyph_name_in_glyph_set(
383                        f"cid{range_start:05d}",
384                        f"cid{range_end:05d}",
385                    )
386                    glyphs.add_cid_range(
387                        range_start,
388                        range_end,
389                        self.make_cid_range_(range_location, range_start, range_end),
390                    )
391                else:
392                    glyph_name = f"cid{self.cur_token_:05d}"
393                    self.check_glyph_name_in_glyph_set(glyph_name)
394                    glyphs.append(glyph_name)
395            elif self.next_token_type_ is Lexer.GLYPHCLASS:
396                self.advance_lexer_()
397                gc = self.glyphclasses_.resolve(self.cur_token_)
398                if gc is None:
399                    raise FeatureLibError(
400                        "Unknown glyph class @%s" % self.cur_token_,
401                        self.cur_token_location_,
402                    )
403                if isinstance(gc, self.ast.MarkClass):
404                    gc = self.ast.MarkClassName(gc, location=self.cur_token_location_)
405                else:
406                    gc = self.ast.GlyphClassName(gc, location=self.cur_token_location_)
407                glyphs.add_class(gc)
408            else:
409                raise FeatureLibError(
410                    "Expected glyph name, glyph range, "
411                    f"or glyph class reference, found {self.next_token_!r}",
412                    self.next_token_location_,
413                )
414        self.expect_symbol_("]")
415        return glyphs
416
417    def parse_glyph_pattern_(self, vertical):
418        # Parses a glyph pattern, including lookups and context, e.g.::
419        #
420        #    a b
421        #    a b c' d e
422        #    a b c' lookup ChangeC d e
423        prefix, glyphs, lookups, values, suffix = ([], [], [], [], [])
424        hasMarks = False
425        while self.next_token_ not in {"by", "from", ";", ","}:
426            gc = self.parse_glyphclass_(accept_glyphname=True)
427            marked = False
428            if self.next_token_ == "'":
429                self.expect_symbol_("'")
430                hasMarks = marked = True
431            if marked:
432                if suffix:
433                    # makeotf also reports this as an error, while FontForge
434                    # silently inserts ' in all the intervening glyphs.
435                    # https://github.com/fonttools/fonttools/pull/1096
436                    raise FeatureLibError(
437                        "Unsupported contextual target sequence: at most "
438                        "one run of marked (') glyph/class names allowed",
439                        self.cur_token_location_,
440                    )
441                glyphs.append(gc)
442            elif glyphs:
443                suffix.append(gc)
444            else:
445                prefix.append(gc)
446
447            if self.is_next_value_():
448                values.append(self.parse_valuerecord_(vertical))
449            else:
450                values.append(None)
451
452            lookuplist = None
453            while self.next_token_ == "lookup":
454                if lookuplist is None:
455                    lookuplist = []
456                self.expect_keyword_("lookup")
457                if not marked:
458                    raise FeatureLibError(
459                        "Lookups can only follow marked glyphs",
460                        self.cur_token_location_,
461                    )
462                lookup_name = self.expect_name_()
463                lookup = self.lookups_.resolve(lookup_name)
464                if lookup is None:
465                    raise FeatureLibError(
466                        'Unknown lookup "%s"' % lookup_name, self.cur_token_location_
467                    )
468                lookuplist.append(lookup)
469            if marked:
470                lookups.append(lookuplist)
471
472        if not glyphs and not suffix:  # eg., "sub f f i by"
473            assert lookups == []
474            return ([], prefix, [None] * len(prefix), values, [], hasMarks)
475        else:
476            assert not any(values[: len(prefix)]), values
477            format1 = values[len(prefix) :][: len(glyphs)]
478            format2 = values[(len(prefix) + len(glyphs)) :][: len(suffix)]
479            values = (
480                format2
481                if format2 and isinstance(format2[0], self.ast.ValueRecord)
482                else format1
483            )
484            return (prefix, glyphs, lookups, values, suffix, hasMarks)
485
486    def parse_chain_context_(self):
487        location = self.cur_token_location_
488        prefix, glyphs, lookups, values, suffix, hasMarks = self.parse_glyph_pattern_(
489            vertical=False
490        )
491        chainContext = [(prefix, glyphs, suffix)]
492        hasLookups = any(lookups)
493        while self.next_token_ == ",":
494            self.expect_symbol_(",")
495            (
496                prefix,
497                glyphs,
498                lookups,
499                values,
500                suffix,
501                hasMarks,
502            ) = self.parse_glyph_pattern_(vertical=False)
503            chainContext.append((prefix, glyphs, suffix))
504            hasLookups = hasLookups or any(lookups)
505        self.expect_symbol_(";")
506        return chainContext, hasLookups
507
508    def parse_ignore_(self):
509        # Parses an ignore sub/pos rule.
510        assert self.is_cur_keyword_("ignore")
511        location = self.cur_token_location_
512        self.advance_lexer_()
513        if self.cur_token_ in ["substitute", "sub"]:
514            chainContext, hasLookups = self.parse_chain_context_()
515            if hasLookups:
516                raise FeatureLibError(
517                    'No lookups can be specified for "ignore sub"', location
518                )
519            return self.ast.IgnoreSubstStatement(chainContext, location=location)
520        if self.cur_token_ in ["position", "pos"]:
521            chainContext, hasLookups = self.parse_chain_context_()
522            if hasLookups:
523                raise FeatureLibError(
524                    'No lookups can be specified for "ignore pos"', location
525                )
526            return self.ast.IgnorePosStatement(chainContext, location=location)
527        raise FeatureLibError(
528            'Expected "substitute" or "position"', self.cur_token_location_
529        )
530
531    def parse_include_(self):
532        assert self.cur_token_ == "include"
533        location = self.cur_token_location_
534        filename = self.expect_filename_()
535        # self.expect_symbol_(";")
536        return ast.IncludeStatement(filename, location=location)
537
538    def parse_language_(self):
539        assert self.is_cur_keyword_("language")
540        location = self.cur_token_location_
541        language = self.expect_language_tag_()
542        include_default, required = (True, False)
543        if self.next_token_ in {"exclude_dflt", "include_dflt"}:
544            include_default = self.expect_name_() == "include_dflt"
545        if self.next_token_ == "required":
546            self.expect_keyword_("required")
547            required = True
548        self.expect_symbol_(";")
549        return self.ast.LanguageStatement(
550            language, include_default, required, location=location
551        )
552
553    def parse_ligatureCaretByIndex_(self):
554        assert self.is_cur_keyword_("LigatureCaretByIndex")
555        location = self.cur_token_location_
556        glyphs = self.parse_glyphclass_(accept_glyphname=True)
557        carets = [self.expect_number_()]
558        while self.next_token_ != ";":
559            carets.append(self.expect_number_())
560        self.expect_symbol_(";")
561        return self.ast.LigatureCaretByIndexStatement(glyphs, carets, location=location)
562
563    def parse_ligatureCaretByPos_(self):
564        assert self.is_cur_keyword_("LigatureCaretByPos")
565        location = self.cur_token_location_
566        glyphs = self.parse_glyphclass_(accept_glyphname=True)
567        carets = [self.expect_number_()]
568        while self.next_token_ != ";":
569            carets.append(self.expect_number_())
570        self.expect_symbol_(";")
571        return self.ast.LigatureCaretByPosStatement(glyphs, carets, location=location)
572
573    def parse_lookup_(self, vertical):
574        # Parses a ``lookup`` - either a lookup block, or a lookup reference
575        # inside a feature.
576        assert self.is_cur_keyword_("lookup")
577        location, name = self.cur_token_location_, self.expect_name_()
578
579        if self.next_token_ == ";":
580            lookup = self.lookups_.resolve(name)
581            if lookup is None:
582                raise FeatureLibError(
583                    'Unknown lookup "%s"' % name, self.cur_token_location_
584                )
585            self.expect_symbol_(";")
586            return self.ast.LookupReferenceStatement(lookup, location=location)
587
588        use_extension = False
589        if self.next_token_ == "useExtension":
590            self.expect_keyword_("useExtension")
591            use_extension = True
592
593        block = self.ast.LookupBlock(name, use_extension, location=location)
594        self.parse_block_(block, vertical)
595        self.lookups_.define(name, block)
596        return block
597
598    def parse_lookupflag_(self):
599        # Parses a ``lookupflag`` statement, either specified by number or
600        # in words.
601        assert self.is_cur_keyword_("lookupflag")
602        location = self.cur_token_location_
603
604        # format B: "lookupflag 6;"
605        if self.next_token_type_ == Lexer.NUMBER:
606            value = self.expect_number_()
607            self.expect_symbol_(";")
608            return self.ast.LookupFlagStatement(value, location=location)
609
610        # format A: "lookupflag RightToLeft MarkAttachmentType @M;"
611        value_seen = False
612        value, markAttachment, markFilteringSet = 0, None, None
613        flags = {
614            "RightToLeft": 1,
615            "IgnoreBaseGlyphs": 2,
616            "IgnoreLigatures": 4,
617            "IgnoreMarks": 8,
618        }
619        seen = set()
620        while self.next_token_ != ";":
621            if self.next_token_ in seen:
622                raise FeatureLibError(
623                    "%s can be specified only once" % self.next_token_,
624                    self.next_token_location_,
625                )
626            seen.add(self.next_token_)
627            if self.next_token_ == "MarkAttachmentType":
628                self.expect_keyword_("MarkAttachmentType")
629                markAttachment = self.parse_glyphclass_(accept_glyphname=False)
630            elif self.next_token_ == "UseMarkFilteringSet":
631                self.expect_keyword_("UseMarkFilteringSet")
632                markFilteringSet = self.parse_glyphclass_(accept_glyphname=False)
633            elif self.next_token_ in flags:
634                value_seen = True
635                value = value | flags[self.expect_name_()]
636            else:
637                raise FeatureLibError(
638                    '"%s" is not a recognized lookupflag' % self.next_token_,
639                    self.next_token_location_,
640                )
641        self.expect_symbol_(";")
642
643        if not any([value_seen, markAttachment, markFilteringSet]):
644            raise FeatureLibError(
645                "lookupflag must have a value", self.next_token_location_
646            )
647
648        return self.ast.LookupFlagStatement(
649            value,
650            markAttachment=markAttachment,
651            markFilteringSet=markFilteringSet,
652            location=location,
653        )
654
655    def parse_markClass_(self):
656        assert self.is_cur_keyword_("markClass")
657        location = self.cur_token_location_
658        glyphs = self.parse_glyphclass_(accept_glyphname=True)
659        anchor = self.parse_anchor_()
660        name = self.expect_class_name_()
661        self.expect_symbol_(";")
662        markClass = self.doc_.markClasses.get(name)
663        if markClass is None:
664            markClass = self.ast.MarkClass(name)
665            self.doc_.markClasses[name] = markClass
666            self.glyphclasses_.define(name, markClass)
667        mcdef = self.ast.MarkClassDefinition(
668            markClass, anchor, glyphs, location=location
669        )
670        markClass.addDefinition(mcdef)
671        return mcdef
672
673    def parse_position_(self, enumerated, vertical):
674        assert self.cur_token_ in {"position", "pos"}
675        if self.next_token_ == "cursive":  # GPOS type 3
676            return self.parse_position_cursive_(enumerated, vertical)
677        elif self.next_token_ == "base":  # GPOS type 4
678            return self.parse_position_base_(enumerated, vertical)
679        elif self.next_token_ == "ligature":  # GPOS type 5
680            return self.parse_position_ligature_(enumerated, vertical)
681        elif self.next_token_ == "mark":  # GPOS type 6
682            return self.parse_position_mark_(enumerated, vertical)
683
684        location = self.cur_token_location_
685        prefix, glyphs, lookups, values, suffix, hasMarks = self.parse_glyph_pattern_(
686            vertical
687        )
688        self.expect_symbol_(";")
689
690        if any(lookups):
691            # GPOS type 8: Chaining contextual positioning; explicit lookups
692            if any(values):
693                raise FeatureLibError(
694                    'If "lookup" is present, no values must be specified', location
695                )
696            return self.ast.ChainContextPosStatement(
697                prefix, glyphs, suffix, lookups, location=location
698            )
699
700        # Pair positioning, format A: "pos V 10 A -10;"
701        # Pair positioning, format B: "pos V A -20;"
702        if not prefix and not suffix and len(glyphs) == 2 and not hasMarks:
703            if values[0] is None:  # Format B: "pos V A -20;"
704                values.reverse()
705            return self.ast.PairPosStatement(
706                glyphs[0],
707                values[0],
708                glyphs[1],
709                values[1],
710                enumerated=enumerated,
711                location=location,
712            )
713
714        if enumerated:
715            raise FeatureLibError(
716                '"enumerate" is only allowed with pair positionings', location
717            )
718        return self.ast.SinglePosStatement(
719            list(zip(glyphs, values)),
720            prefix,
721            suffix,
722            forceChain=hasMarks,
723            location=location,
724        )
725
726    def parse_position_cursive_(self, enumerated, vertical):
727        location = self.cur_token_location_
728        self.expect_keyword_("cursive")
729        if enumerated:
730            raise FeatureLibError(
731                '"enumerate" is not allowed with ' "cursive attachment positioning",
732                location,
733            )
734        glyphclass = self.parse_glyphclass_(accept_glyphname=True)
735        entryAnchor = self.parse_anchor_()
736        exitAnchor = self.parse_anchor_()
737        self.expect_symbol_(";")
738        return self.ast.CursivePosStatement(
739            glyphclass, entryAnchor, exitAnchor, location=location
740        )
741
742    def parse_position_base_(self, enumerated, vertical):
743        location = self.cur_token_location_
744        self.expect_keyword_("base")
745        if enumerated:
746            raise FeatureLibError(
747                '"enumerate" is not allowed with '
748                "mark-to-base attachment positioning",
749                location,
750            )
751        base = self.parse_glyphclass_(accept_glyphname=True)
752        marks = self.parse_anchor_marks_()
753        self.expect_symbol_(";")
754        return self.ast.MarkBasePosStatement(base, marks, location=location)
755
756    def parse_position_ligature_(self, enumerated, vertical):
757        location = self.cur_token_location_
758        self.expect_keyword_("ligature")
759        if enumerated:
760            raise FeatureLibError(
761                '"enumerate" is not allowed with '
762                "mark-to-ligature attachment positioning",
763                location,
764            )
765        ligatures = self.parse_glyphclass_(accept_glyphname=True)
766        marks = [self.parse_anchor_marks_()]
767        while self.next_token_ == "ligComponent":
768            self.expect_keyword_("ligComponent")
769            marks.append(self.parse_anchor_marks_())
770        self.expect_symbol_(";")
771        return self.ast.MarkLigPosStatement(ligatures, marks, location=location)
772
773    def parse_position_mark_(self, enumerated, vertical):
774        location = self.cur_token_location_
775        self.expect_keyword_("mark")
776        if enumerated:
777            raise FeatureLibError(
778                '"enumerate" is not allowed with '
779                "mark-to-mark attachment positioning",
780                location,
781            )
782        baseMarks = self.parse_glyphclass_(accept_glyphname=True)
783        marks = self.parse_anchor_marks_()
784        self.expect_symbol_(";")
785        return self.ast.MarkMarkPosStatement(baseMarks, marks, location=location)
786
787    def parse_script_(self):
788        assert self.is_cur_keyword_("script")
789        location, script = self.cur_token_location_, self.expect_script_tag_()
790        self.expect_symbol_(";")
791        return self.ast.ScriptStatement(script, location=location)
792
793    def parse_substitute_(self):
794        assert self.cur_token_ in {"substitute", "sub", "reversesub", "rsub"}
795        location = self.cur_token_location_
796        reverse = self.cur_token_ in {"reversesub", "rsub"}
797        (
798            old_prefix,
799            old,
800            lookups,
801            values,
802            old_suffix,
803            hasMarks,
804        ) = self.parse_glyph_pattern_(vertical=False)
805        if any(values):
806            raise FeatureLibError(
807                "Substitution statements cannot contain values", location
808            )
809        new = []
810        if self.next_token_ == "by":
811            keyword = self.expect_keyword_("by")
812            while self.next_token_ != ";":
813                gc = self.parse_glyphclass_(accept_glyphname=True, accept_null=True)
814                new.append(gc)
815        elif self.next_token_ == "from":
816            keyword = self.expect_keyword_("from")
817            new = [self.parse_glyphclass_(accept_glyphname=False)]
818        else:
819            keyword = None
820        self.expect_symbol_(";")
821        if len(new) == 0 and not any(lookups):
822            raise FeatureLibError(
823                'Expected "by", "from" or explicit lookup references',
824                self.cur_token_location_,
825            )
826
827        # GSUB lookup type 3: Alternate substitution.
828        # Format: "substitute a from [a.1 a.2 a.3];"
829        if keyword == "from":
830            if reverse:
831                raise FeatureLibError(
832                    'Reverse chaining substitutions do not support "from"', location
833                )
834            if len(old) != 1 or len(old[0].glyphSet()) != 1:
835                raise FeatureLibError('Expected a single glyph before "from"', location)
836            if len(new) != 1:
837                raise FeatureLibError(
838                    'Expected a single glyphclass after "from"', location
839                )
840            return self.ast.AlternateSubstStatement(
841                old_prefix, old[0], old_suffix, new[0], location=location
842            )
843
844        num_lookups = len([l for l in lookups if l is not None])
845
846        is_deletion = False
847        if len(new) == 1 and len(new[0].glyphSet()) == 0:
848            new = []  # Deletion
849            is_deletion = True
850
851        # GSUB lookup type 1: Single substitution.
852        # Format A: "substitute a by a.sc;"
853        # Format B: "substitute [one.fitted one.oldstyle] by one;"
854        # Format C: "substitute [a-d] by [A.sc-D.sc];"
855        if not reverse and len(old) == 1 and len(new) == 1 and num_lookups == 0:
856            glyphs = list(old[0].glyphSet())
857            replacements = list(new[0].glyphSet())
858            if len(replacements) == 1:
859                replacements = replacements * len(glyphs)
860            if len(glyphs) != len(replacements):
861                raise FeatureLibError(
862                    'Expected a glyph class with %d elements after "by", '
863                    "but found a glyph class with %d elements"
864                    % (len(glyphs), len(replacements)),
865                    location,
866                )
867            return self.ast.SingleSubstStatement(
868                old, new, old_prefix, old_suffix, forceChain=hasMarks, location=location
869            )
870
871        # GSUB lookup type 2: Multiple substitution.
872        # Format: "substitute f_f_i by f f i;"
873        if (
874            not reverse
875            and len(old) == 1
876            and len(old[0].glyphSet()) == 1
877            and (
878                (len(new) > 1 and max([len(n.glyphSet()) for n in new]) == 1)
879                or len(new) == 0
880            )
881            and num_lookups == 0
882        ):
883            return self.ast.MultipleSubstStatement(
884                old_prefix,
885                tuple(old[0].glyphSet())[0],
886                old_suffix,
887                tuple([list(n.glyphSet())[0] for n in new]),
888                forceChain=hasMarks,
889                location=location,
890            )
891
892        # GSUB lookup type 4: Ligature substitution.
893        # Format: "substitute f f i by f_f_i;"
894        if (
895            not reverse
896            and len(old) > 1
897            and len(new) == 1
898            and len(new[0].glyphSet()) == 1
899            and num_lookups == 0
900        ):
901            return self.ast.LigatureSubstStatement(
902                old_prefix,
903                old,
904                old_suffix,
905                list(new[0].glyphSet())[0],
906                forceChain=hasMarks,
907                location=location,
908            )
909
910        # GSUB lookup type 8: Reverse chaining substitution.
911        if reverse:
912            if len(old) != 1:
913                raise FeatureLibError(
914                    "In reverse chaining single substitutions, "
915                    "only a single glyph or glyph class can be replaced",
916                    location,
917                )
918            if len(new) != 1:
919                raise FeatureLibError(
920                    "In reverse chaining single substitutions, "
921                    'the replacement (after "by") must be a single glyph '
922                    "or glyph class",
923                    location,
924                )
925            if num_lookups != 0:
926                raise FeatureLibError(
927                    "Reverse chaining substitutions cannot call named lookups", location
928                )
929            glyphs = sorted(list(old[0].glyphSet()))
930            replacements = sorted(list(new[0].glyphSet()))
931            if len(replacements) == 1:
932                replacements = replacements * len(glyphs)
933            if len(glyphs) != len(replacements):
934                raise FeatureLibError(
935                    'Expected a glyph class with %d elements after "by", '
936                    "but found a glyph class with %d elements"
937                    % (len(glyphs), len(replacements)),
938                    location,
939                )
940            return self.ast.ReverseChainSingleSubstStatement(
941                old_prefix, old_suffix, old, new, location=location
942            )
943
944        if len(old) > 1 and len(new) > 1:
945            raise FeatureLibError(
946                "Direct substitution of multiple glyphs by multiple glyphs "
947                "is not supported",
948                location,
949            )
950
951        # If there are remaining glyphs to parse, this is an invalid GSUB statement
952        if len(new) != 0 or is_deletion:
953            raise FeatureLibError("Invalid substitution statement", location)
954
955        # GSUB lookup type 6: Chaining contextual substitution.
956        rule = self.ast.ChainContextSubstStatement(
957            old_prefix, old, old_suffix, lookups, location=location
958        )
959        return rule
960
961    def parse_subtable_(self):
962        assert self.is_cur_keyword_("subtable")
963        location = self.cur_token_location_
964        self.expect_symbol_(";")
965        return self.ast.SubtableStatement(location=location)
966
967    def parse_size_parameters_(self):
968        # Parses a ``parameters`` statement used in ``size`` features. See
969        # `section 8.b <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.b>`_.
970        assert self.is_cur_keyword_("parameters")
971        location = self.cur_token_location_
972        DesignSize = self.expect_decipoint_()
973        SubfamilyID = self.expect_number_()
974        RangeStart = 0.
975        RangeEnd = 0.
976        if self.next_token_type_ in (Lexer.NUMBER, Lexer.FLOAT) or SubfamilyID != 0:
977            RangeStart = self.expect_decipoint_()
978            RangeEnd = self.expect_decipoint_()
979
980        self.expect_symbol_(";")
981        return self.ast.SizeParameters(
982            DesignSize, SubfamilyID, RangeStart, RangeEnd, location=location
983        )
984
985    def parse_size_menuname_(self):
986        assert self.is_cur_keyword_("sizemenuname")
987        location = self.cur_token_location_
988        platformID, platEncID, langID, string = self.parse_name_()
989        return self.ast.FeatureNameStatement(
990            "size", platformID, platEncID, langID, string, location=location
991        )
992
993    def parse_table_(self):
994        assert self.is_cur_keyword_("table")
995        location, name = self.cur_token_location_, self.expect_tag_()
996        table = self.ast.TableBlock(name, location=location)
997        self.expect_symbol_("{")
998        handler = {
999            "GDEF": self.parse_table_GDEF_,
1000            "head": self.parse_table_head_,
1001            "hhea": self.parse_table_hhea_,
1002            "vhea": self.parse_table_vhea_,
1003            "name": self.parse_table_name_,
1004            "BASE": self.parse_table_BASE_,
1005            "OS/2": self.parse_table_OS_2_,
1006            "STAT": self.parse_table_STAT_,
1007        }.get(name)
1008        if handler:
1009            handler(table)
1010        else:
1011            raise FeatureLibError(
1012                '"table %s" is not supported' % name.strip(), location
1013            )
1014        self.expect_symbol_("}")
1015        end_tag = self.expect_tag_()
1016        if end_tag != name:
1017            raise FeatureLibError(
1018                'Expected "%s"' % name.strip(), self.cur_token_location_
1019            )
1020        self.expect_symbol_(";")
1021        return table
1022
1023    def parse_table_GDEF_(self, table):
1024        statements = table.statements
1025        while self.next_token_ != "}" or self.cur_comments_:
1026            self.advance_lexer_(comments=True)
1027            if self.cur_token_type_ is Lexer.COMMENT:
1028                statements.append(
1029                    self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
1030                )
1031            elif self.is_cur_keyword_("Attach"):
1032                statements.append(self.parse_attach_())
1033            elif self.is_cur_keyword_("GlyphClassDef"):
1034                statements.append(self.parse_GlyphClassDef_())
1035            elif self.is_cur_keyword_("LigatureCaretByIndex"):
1036                statements.append(self.parse_ligatureCaretByIndex_())
1037            elif self.is_cur_keyword_("LigatureCaretByPos"):
1038                statements.append(self.parse_ligatureCaretByPos_())
1039            elif self.cur_token_ == ";":
1040                continue
1041            else:
1042                raise FeatureLibError(
1043                    "Expected Attach, LigatureCaretByIndex, " "or LigatureCaretByPos",
1044                    self.cur_token_location_,
1045                )
1046
1047    def parse_table_head_(self, table):
1048        statements = table.statements
1049        while self.next_token_ != "}" or self.cur_comments_:
1050            self.advance_lexer_(comments=True)
1051            if self.cur_token_type_ is Lexer.COMMENT:
1052                statements.append(
1053                    self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
1054                )
1055            elif self.is_cur_keyword_("FontRevision"):
1056                statements.append(self.parse_FontRevision_())
1057            elif self.cur_token_ == ";":
1058                continue
1059            else:
1060                raise FeatureLibError("Expected FontRevision", self.cur_token_location_)
1061
1062    def parse_table_hhea_(self, table):
1063        statements = table.statements
1064        fields = ("CaretOffset", "Ascender", "Descender", "LineGap")
1065        while self.next_token_ != "}" or self.cur_comments_:
1066            self.advance_lexer_(comments=True)
1067            if self.cur_token_type_ is Lexer.COMMENT:
1068                statements.append(
1069                    self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
1070                )
1071            elif self.cur_token_type_ is Lexer.NAME and self.cur_token_ in fields:
1072                key = self.cur_token_.lower()
1073                value = self.expect_number_()
1074                statements.append(
1075                    self.ast.HheaField(key, value, location=self.cur_token_location_)
1076                )
1077                if self.next_token_ != ";":
1078                    raise FeatureLibError(
1079                        "Incomplete statement", self.next_token_location_
1080                    )
1081            elif self.cur_token_ == ";":
1082                continue
1083            else:
1084                raise FeatureLibError(
1085                    "Expected CaretOffset, Ascender, " "Descender or LineGap",
1086                    self.cur_token_location_,
1087                )
1088
1089    def parse_table_vhea_(self, table):
1090        statements = table.statements
1091        fields = ("VertTypoAscender", "VertTypoDescender", "VertTypoLineGap")
1092        while self.next_token_ != "}" or self.cur_comments_:
1093            self.advance_lexer_(comments=True)
1094            if self.cur_token_type_ is Lexer.COMMENT:
1095                statements.append(
1096                    self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
1097                )
1098            elif self.cur_token_type_ is Lexer.NAME and self.cur_token_ in fields:
1099                key = self.cur_token_.lower()
1100                value = self.expect_number_()
1101                statements.append(
1102                    self.ast.VheaField(key, value, location=self.cur_token_location_)
1103                )
1104                if self.next_token_ != ";":
1105                    raise FeatureLibError(
1106                        "Incomplete statement", self.next_token_location_
1107                    )
1108            elif self.cur_token_ == ";":
1109                continue
1110            else:
1111                raise FeatureLibError(
1112                    "Expected VertTypoAscender, "
1113                    "VertTypoDescender or VertTypoLineGap",
1114                    self.cur_token_location_,
1115                )
1116
1117    def parse_table_name_(self, table):
1118        statements = table.statements
1119        while self.next_token_ != "}" or self.cur_comments_:
1120            self.advance_lexer_(comments=True)
1121            if self.cur_token_type_ is Lexer.COMMENT:
1122                statements.append(
1123                    self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
1124                )
1125            elif self.is_cur_keyword_("nameid"):
1126                statement = self.parse_nameid_()
1127                if statement:
1128                    statements.append(statement)
1129            elif self.cur_token_ == ";":
1130                continue
1131            else:
1132                raise FeatureLibError("Expected nameid", self.cur_token_location_)
1133
1134    def parse_name_(self):
1135        """Parses a name record. See `section 9.e <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.e>`_."""
1136        platEncID = None
1137        langID = None
1138        if self.next_token_type_ in Lexer.NUMBERS:
1139            platformID = self.expect_any_number_()
1140            location = self.cur_token_location_
1141            if platformID not in (1, 3):
1142                raise FeatureLibError("Expected platform id 1 or 3", location)
1143            if self.next_token_type_ in Lexer.NUMBERS:
1144                platEncID = self.expect_any_number_()
1145                langID = self.expect_any_number_()
1146        else:
1147            platformID = 3
1148            location = self.cur_token_location_
1149
1150        if platformID == 1:  # Macintosh
1151            platEncID = platEncID or 0  # Roman
1152            langID = langID or 0  # English
1153        else:  # 3, Windows
1154            platEncID = platEncID or 1  # Unicode
1155            langID = langID or 0x0409  # English
1156
1157        string = self.expect_string_()
1158        self.expect_symbol_(";")
1159
1160        encoding = getEncoding(platformID, platEncID, langID)
1161        if encoding is None:
1162            raise FeatureLibError("Unsupported encoding", location)
1163        unescaped = self.unescape_string_(string, encoding)
1164        return platformID, platEncID, langID, unescaped
1165
1166    def parse_stat_name_(self):
1167        platEncID = None
1168        langID = None
1169        if self.next_token_type_ in Lexer.NUMBERS:
1170            platformID = self.expect_any_number_()
1171            location = self.cur_token_location_
1172            if platformID not in (1, 3):
1173                raise FeatureLibError("Expected platform id 1 or 3", location)
1174            if self.next_token_type_ in Lexer.NUMBERS:
1175                platEncID = self.expect_any_number_()
1176                langID = self.expect_any_number_()
1177        else:
1178            platformID = 3
1179            location = self.cur_token_location_
1180
1181        if platformID == 1:  # Macintosh
1182            platEncID = platEncID or 0  # Roman
1183            langID = langID or 0  # English
1184        else:  # 3, Windows
1185            platEncID = platEncID or 1  # Unicode
1186            langID = langID or 0x0409  # English
1187
1188        string = self.expect_string_()
1189        encoding = getEncoding(platformID, platEncID, langID)
1190        if encoding is None:
1191            raise FeatureLibError("Unsupported encoding", location)
1192        unescaped = self.unescape_string_(string, encoding)
1193        return platformID, platEncID, langID, unescaped
1194
1195    def parse_nameid_(self):
1196        assert self.cur_token_ == "nameid", self.cur_token_
1197        location, nameID = self.cur_token_location_, self.expect_any_number_()
1198        if nameID > 32767:
1199            raise FeatureLibError(
1200                "Name id value cannot be greater than 32767", self.cur_token_location_
1201            )
1202        if 1 <= nameID <= 6:
1203            log.warning(
1204                "Name id %d cannot be set from the feature file. "
1205                "Ignoring record" % nameID
1206            )
1207            self.parse_name_()  # skip to the next record
1208            return None
1209
1210        platformID, platEncID, langID, string = self.parse_name_()
1211        return self.ast.NameRecord(
1212            nameID, platformID, platEncID, langID, string, location=location
1213        )
1214
1215    def unescape_string_(self, string, encoding):
1216        if encoding == "utf_16_be":
1217            s = re.sub(r"\\[0-9a-fA-F]{4}", self.unescape_unichr_, string)
1218        else:
1219            unescape = lambda m: self.unescape_byte_(m, encoding)
1220            s = re.sub(r"\\[0-9a-fA-F]{2}", unescape, string)
1221        # We now have a Unicode string, but it might contain surrogate pairs.
1222        # We convert surrogates to actual Unicode by round-tripping through
1223        # Python's UTF-16 codec in a special mode.
1224        utf16 = tobytes(s, "utf_16_be", "surrogatepass")
1225        return tostr(utf16, "utf_16_be")
1226
1227    @staticmethod
1228    def unescape_unichr_(match):
1229        n = match.group(0)[1:]
1230        return chr(int(n, 16))
1231
1232    @staticmethod
1233    def unescape_byte_(match, encoding):
1234        n = match.group(0)[1:]
1235        return bytechr(int(n, 16)).decode(encoding)
1236
1237    def parse_table_BASE_(self, table):
1238        statements = table.statements
1239        while self.next_token_ != "}" or self.cur_comments_:
1240            self.advance_lexer_(comments=True)
1241            if self.cur_token_type_ is Lexer.COMMENT:
1242                statements.append(
1243                    self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
1244                )
1245            elif self.is_cur_keyword_("HorizAxis.BaseTagList"):
1246                horiz_bases = self.parse_base_tag_list_()
1247            elif self.is_cur_keyword_("HorizAxis.BaseScriptList"):
1248                horiz_scripts = self.parse_base_script_list_(len(horiz_bases))
1249                statements.append(
1250                    self.ast.BaseAxis(
1251                        horiz_bases,
1252                        horiz_scripts,
1253                        False,
1254                        location=self.cur_token_location_,
1255                    )
1256                )
1257            elif self.is_cur_keyword_("VertAxis.BaseTagList"):
1258                vert_bases = self.parse_base_tag_list_()
1259            elif self.is_cur_keyword_("VertAxis.BaseScriptList"):
1260                vert_scripts = self.parse_base_script_list_(len(vert_bases))
1261                statements.append(
1262                    self.ast.BaseAxis(
1263                        vert_bases,
1264                        vert_scripts,
1265                        True,
1266                        location=self.cur_token_location_,
1267                    )
1268                )
1269            elif self.cur_token_ == ";":
1270                continue
1271
1272    def parse_table_OS_2_(self, table):
1273        statements = table.statements
1274        numbers = (
1275            "FSType",
1276            "TypoAscender",
1277            "TypoDescender",
1278            "TypoLineGap",
1279            "winAscent",
1280            "winDescent",
1281            "XHeight",
1282            "CapHeight",
1283            "WeightClass",
1284            "WidthClass",
1285            "LowerOpSize",
1286            "UpperOpSize",
1287        )
1288        ranges = ("UnicodeRange", "CodePageRange")
1289        while self.next_token_ != "}" or self.cur_comments_:
1290            self.advance_lexer_(comments=True)
1291            if self.cur_token_type_ is Lexer.COMMENT:
1292                statements.append(
1293                    self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
1294                )
1295            elif self.cur_token_type_ is Lexer.NAME:
1296                key = self.cur_token_.lower()
1297                value = None
1298                if self.cur_token_ in numbers:
1299                    value = self.expect_number_()
1300                elif self.is_cur_keyword_("Panose"):
1301                    value = []
1302                    for i in range(10):
1303                        value.append(self.expect_number_())
1304                elif self.cur_token_ in ranges:
1305                    value = []
1306                    while self.next_token_ != ";":
1307                        value.append(self.expect_number_())
1308                elif self.is_cur_keyword_("Vendor"):
1309                    value = self.expect_string_()
1310                statements.append(
1311                    self.ast.OS2Field(key, value, location=self.cur_token_location_)
1312                )
1313            elif self.cur_token_ == ";":
1314                continue
1315
1316    def parse_STAT_ElidedFallbackName(self):
1317        assert self.is_cur_keyword_("ElidedFallbackName")
1318        self.expect_symbol_("{")
1319        names = []
1320        while self.next_token_ != "}" or self.cur_comments_:
1321            self.advance_lexer_()
1322            if self.is_cur_keyword_("name"):
1323                platformID, platEncID, langID, string = self.parse_stat_name_()
1324                nameRecord = self.ast.STATNameStatement(
1325                    "stat",
1326                    platformID,
1327                    platEncID,
1328                    langID,
1329                    string,
1330                    location=self.cur_token_location_,
1331                )
1332                names.append(nameRecord)
1333            else:
1334                if self.cur_token_ != ";":
1335                    raise FeatureLibError(
1336                        f"Unexpected token {self.cur_token_} " f"in ElidedFallbackName",
1337                        self.cur_token_location_,
1338                    )
1339        self.expect_symbol_("}")
1340        if not names:
1341            raise FeatureLibError('Expected "name"', self.cur_token_location_)
1342        return names
1343
1344    def parse_STAT_design_axis(self):
1345        assert self.is_cur_keyword_("DesignAxis")
1346        names = []
1347        axisTag = self.expect_tag_()
1348        if (
1349            axisTag not in ("ital", "opsz", "slnt", "wdth", "wght")
1350            and not axisTag.isupper()
1351        ):
1352            log.warning(f"Unregistered axis tag {axisTag} should be uppercase.")
1353        axisOrder = self.expect_number_()
1354        self.expect_symbol_("{")
1355        while self.next_token_ != "}" or self.cur_comments_:
1356            self.advance_lexer_()
1357            if self.cur_token_type_ is Lexer.COMMENT:
1358                continue
1359            elif self.is_cur_keyword_("name"):
1360                location = self.cur_token_location_
1361                platformID, platEncID, langID, string = self.parse_stat_name_()
1362                name = self.ast.STATNameStatement(
1363                    "stat", platformID, platEncID, langID, string, location=location
1364                )
1365                names.append(name)
1366            elif self.cur_token_ == ";":
1367                continue
1368            else:
1369                raise FeatureLibError(
1370                    f'Expected "name", got {self.cur_token_}', self.cur_token_location_
1371                )
1372
1373        self.expect_symbol_("}")
1374        return self.ast.STATDesignAxisStatement(
1375            axisTag, axisOrder, names, self.cur_token_location_
1376        )
1377
1378    def parse_STAT_axis_value_(self):
1379        assert self.is_cur_keyword_("AxisValue")
1380        self.expect_symbol_("{")
1381        locations = []
1382        names = []
1383        flags = 0
1384        while self.next_token_ != "}" or self.cur_comments_:
1385            self.advance_lexer_(comments=True)
1386            if self.cur_token_type_ is Lexer.COMMENT:
1387                continue
1388            elif self.is_cur_keyword_("name"):
1389                location = self.cur_token_location_
1390                platformID, platEncID, langID, string = self.parse_stat_name_()
1391                name = self.ast.STATNameStatement(
1392                    "stat", platformID, platEncID, langID, string, location=location
1393                )
1394                names.append(name)
1395            elif self.is_cur_keyword_("location"):
1396                location = self.parse_STAT_location()
1397                locations.append(location)
1398            elif self.is_cur_keyword_("flag"):
1399                flags = self.expect_stat_flags()
1400            elif self.cur_token_ == ";":
1401                continue
1402            else:
1403                raise FeatureLibError(
1404                    f"Unexpected token {self.cur_token_} " f"in AxisValue",
1405                    self.cur_token_location_,
1406                )
1407        self.expect_symbol_("}")
1408        if not names:
1409            raise FeatureLibError('Expected "Axis Name"', self.cur_token_location_)
1410        if not locations:
1411            raise FeatureLibError('Expected "Axis location"', self.cur_token_location_)
1412        if len(locations) > 1:
1413            for location in locations:
1414                if len(location.values) > 1:
1415                    raise FeatureLibError(
1416                        "Only one value is allowed in a "
1417                        "Format 4 Axis Value Record, but "
1418                        f"{len(location.values)} were found.",
1419                        self.cur_token_location_,
1420                    )
1421            format4_tags = []
1422            for location in locations:
1423                tag = location.tag
1424                if tag in format4_tags:
1425                    raise FeatureLibError(
1426                        f"Axis tag {tag} already " "defined.", self.cur_token_location_
1427                    )
1428                format4_tags.append(tag)
1429
1430        return self.ast.STATAxisValueStatement(
1431            names, locations, flags, self.cur_token_location_
1432        )
1433
1434    def parse_STAT_location(self):
1435        values = []
1436        tag = self.expect_tag_()
1437        if len(tag.strip()) != 4:
1438            raise FeatureLibError(
1439                f"Axis tag {self.cur_token_} must be 4 " "characters",
1440                self.cur_token_location_,
1441            )
1442
1443        while self.next_token_ != ";":
1444            if self.next_token_type_ is Lexer.FLOAT:
1445                value = self.expect_float_()
1446                values.append(value)
1447            elif self.next_token_type_ is Lexer.NUMBER:
1448                value = self.expect_number_()
1449                values.append(value)
1450            else:
1451                raise FeatureLibError(
1452                    f'Unexpected value "{self.next_token_}". '
1453                    "Expected integer or float.",
1454                    self.next_token_location_,
1455                )
1456        if len(values) == 3:
1457            nominal, min_val, max_val = values
1458            if nominal < min_val or nominal > max_val:
1459                raise FeatureLibError(
1460                    f"Default value {nominal} is outside "
1461                    f"of specified range "
1462                    f"{min_val}-{max_val}.",
1463                    self.next_token_location_,
1464                )
1465        return self.ast.AxisValueLocationStatement(tag, values)
1466
1467    def parse_table_STAT_(self, table):
1468        statements = table.statements
1469        design_axes = []
1470        while self.next_token_ != "}" or self.cur_comments_:
1471            self.advance_lexer_(comments=True)
1472            if self.cur_token_type_ is Lexer.COMMENT:
1473                statements.append(
1474                    self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
1475                )
1476            elif self.cur_token_type_ is Lexer.NAME:
1477                if self.is_cur_keyword_("ElidedFallbackName"):
1478                    names = self.parse_STAT_ElidedFallbackName()
1479                    statements.append(self.ast.ElidedFallbackName(names))
1480                elif self.is_cur_keyword_("ElidedFallbackNameID"):
1481                    value = self.expect_number_()
1482                    statements.append(self.ast.ElidedFallbackNameID(value))
1483                    self.expect_symbol_(";")
1484                elif self.is_cur_keyword_("DesignAxis"):
1485                    designAxis = self.parse_STAT_design_axis()
1486                    design_axes.append(designAxis.tag)
1487                    statements.append(designAxis)
1488                    self.expect_symbol_(";")
1489                elif self.is_cur_keyword_("AxisValue"):
1490                    axisValueRecord = self.parse_STAT_axis_value_()
1491                    for location in axisValueRecord.locations:
1492                        if location.tag not in design_axes:
1493                            # Tag must be defined in a DesignAxis before it
1494                            # can be referenced
1495                            raise FeatureLibError(
1496                                "DesignAxis not defined for " f"{location.tag}.",
1497                                self.cur_token_location_,
1498                            )
1499                    statements.append(axisValueRecord)
1500                    self.expect_symbol_(";")
1501                else:
1502                    raise FeatureLibError(
1503                        f"Unexpected token {self.cur_token_}", self.cur_token_location_
1504                    )
1505            elif self.cur_token_ == ";":
1506                continue
1507
1508    def parse_base_tag_list_(self):
1509        # Parses BASE table entries. (See `section 9.a <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.a>`_)
1510        assert self.cur_token_ in (
1511            "HorizAxis.BaseTagList",
1512            "VertAxis.BaseTagList",
1513        ), self.cur_token_
1514        bases = []
1515        while self.next_token_ != ";":
1516            bases.append(self.expect_script_tag_())
1517        self.expect_symbol_(";")
1518        return bases
1519
1520    def parse_base_script_list_(self, count):
1521        assert self.cur_token_ in (
1522            "HorizAxis.BaseScriptList",
1523            "VertAxis.BaseScriptList",
1524        ), self.cur_token_
1525        scripts = [(self.parse_base_script_record_(count))]
1526        while self.next_token_ == ",":
1527            self.expect_symbol_(",")
1528            scripts.append(self.parse_base_script_record_(count))
1529        self.expect_symbol_(";")
1530        return scripts
1531
1532    def parse_base_script_record_(self, count):
1533        script_tag = self.expect_script_tag_()
1534        base_tag = self.expect_script_tag_()
1535        coords = [self.expect_number_() for i in range(count)]
1536        return script_tag, base_tag, coords
1537
1538    def parse_device_(self):
1539        result = None
1540        self.expect_symbol_("<")
1541        self.expect_keyword_("device")
1542        if self.next_token_ == "NULL":
1543            self.expect_keyword_("NULL")
1544        else:
1545            result = [(self.expect_number_(), self.expect_number_())]
1546            while self.next_token_ == ",":
1547                self.expect_symbol_(",")
1548                result.append((self.expect_number_(), self.expect_number_()))
1549            result = tuple(result)  # make it hashable
1550        self.expect_symbol_(">")
1551        return result
1552
1553    def is_next_value_(self):
1554        return self.next_token_type_ is Lexer.NUMBER or self.next_token_ == "<"
1555
1556    def parse_valuerecord_(self, vertical):
1557        if self.next_token_type_ is Lexer.NUMBER:
1558            number, location = self.expect_number_(), self.cur_token_location_
1559            if vertical:
1560                val = self.ast.ValueRecord(
1561                    yAdvance=number, vertical=vertical, location=location
1562                )
1563            else:
1564                val = self.ast.ValueRecord(
1565                    xAdvance=number, vertical=vertical, location=location
1566                )
1567            return val
1568        self.expect_symbol_("<")
1569        location = self.cur_token_location_
1570        if self.next_token_type_ is Lexer.NAME:
1571            name = self.expect_name_()
1572            if name == "NULL":
1573                self.expect_symbol_(">")
1574                return self.ast.ValueRecord()
1575            vrd = self.valuerecords_.resolve(name)
1576            if vrd is None:
1577                raise FeatureLibError(
1578                    'Unknown valueRecordDef "%s"' % name, self.cur_token_location_
1579                )
1580            value = vrd.value
1581            xPlacement, yPlacement = (value.xPlacement, value.yPlacement)
1582            xAdvance, yAdvance = (value.xAdvance, value.yAdvance)
1583        else:
1584            xPlacement, yPlacement, xAdvance, yAdvance = (
1585                self.expect_number_(),
1586                self.expect_number_(),
1587                self.expect_number_(),
1588                self.expect_number_(),
1589            )
1590
1591        if self.next_token_ == "<":
1592            xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice = (
1593                self.parse_device_(),
1594                self.parse_device_(),
1595                self.parse_device_(),
1596                self.parse_device_(),
1597            )
1598            allDeltas = sorted(
1599                [
1600                    delta
1601                    for size, delta in (xPlaDevice if xPlaDevice else ())
1602                    + (yPlaDevice if yPlaDevice else ())
1603                    + (xAdvDevice if xAdvDevice else ())
1604                    + (yAdvDevice if yAdvDevice else ())
1605                ]
1606            )
1607            if allDeltas[0] < -128 or allDeltas[-1] > 127:
1608                raise FeatureLibError(
1609                    "Device value out of valid range (-128..127)",
1610                    self.cur_token_location_,
1611                )
1612        else:
1613            xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice = (None, None, None, None)
1614
1615        self.expect_symbol_(">")
1616        return self.ast.ValueRecord(
1617            xPlacement,
1618            yPlacement,
1619            xAdvance,
1620            yAdvance,
1621            xPlaDevice,
1622            yPlaDevice,
1623            xAdvDevice,
1624            yAdvDevice,
1625            vertical=vertical,
1626            location=location,
1627        )
1628
1629    def parse_valuerecord_definition_(self, vertical):
1630        # Parses a named value record definition. (See section `2.e.v <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#2.e.v>`_)
1631        assert self.is_cur_keyword_("valueRecordDef")
1632        location = self.cur_token_location_
1633        value = self.parse_valuerecord_(vertical)
1634        name = self.expect_name_()
1635        self.expect_symbol_(";")
1636        vrd = self.ast.ValueRecordDefinition(name, value, location=location)
1637        self.valuerecords_.define(name, vrd)
1638        return vrd
1639
1640    def parse_languagesystem_(self):
1641        assert self.cur_token_ == "languagesystem"
1642        location = self.cur_token_location_
1643        script = self.expect_script_tag_()
1644        language = self.expect_language_tag_()
1645        self.expect_symbol_(";")
1646        return self.ast.LanguageSystemStatement(script, language, location=location)
1647
1648    def parse_feature_block_(self):
1649        assert self.cur_token_ == "feature"
1650        location = self.cur_token_location_
1651        tag = self.expect_tag_()
1652        vertical = tag in {"vkrn", "vpal", "vhal", "valt"}
1653
1654        stylisticset = None
1655        cv_feature = None
1656        size_feature = False
1657        if tag in self.SS_FEATURE_TAGS:
1658            stylisticset = tag
1659        elif tag in self.CV_FEATURE_TAGS:
1660            cv_feature = tag
1661        elif tag == "size":
1662            size_feature = True
1663
1664        use_extension = False
1665        if self.next_token_ == "useExtension":
1666            self.expect_keyword_("useExtension")
1667            use_extension = True
1668
1669        block = self.ast.FeatureBlock(
1670            tag, use_extension=use_extension, location=location
1671        )
1672        self.parse_block_(block, vertical, stylisticset, size_feature, cv_feature)
1673        return block
1674
1675    def parse_feature_reference_(self):
1676        assert self.cur_token_ == "feature", self.cur_token_
1677        location = self.cur_token_location_
1678        featureName = self.expect_tag_()
1679        self.expect_symbol_(";")
1680        return self.ast.FeatureReferenceStatement(featureName, location=location)
1681
1682    def parse_featureNames_(self, tag):
1683        """Parses a ``featureNames`` statement found in stylistic set features.
1684        See section `8.c <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.c>`_."""
1685        assert self.cur_token_ == "featureNames", self.cur_token_
1686        block = self.ast.NestedBlock(
1687            tag, self.cur_token_, location=self.cur_token_location_
1688        )
1689        self.expect_symbol_("{")
1690        for symtab in self.symbol_tables_:
1691            symtab.enter_scope()
1692        while self.next_token_ != "}" or self.cur_comments_:
1693            self.advance_lexer_(comments=True)
1694            if self.cur_token_type_ is Lexer.COMMENT:
1695                block.statements.append(
1696                    self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
1697                )
1698            elif self.is_cur_keyword_("name"):
1699                location = self.cur_token_location_
1700                platformID, platEncID, langID, string = self.parse_name_()
1701                block.statements.append(
1702                    self.ast.FeatureNameStatement(
1703                        tag, platformID, platEncID, langID, string, location=location
1704                    )
1705                )
1706            elif self.cur_token_ == ";":
1707                continue
1708            else:
1709                raise FeatureLibError('Expected "name"', self.cur_token_location_)
1710        self.expect_symbol_("}")
1711        for symtab in self.symbol_tables_:
1712            symtab.exit_scope()
1713        self.expect_symbol_(";")
1714        return block
1715
1716    def parse_cvParameters_(self, tag):
1717        # Parses a ``cvParameters`` block found in Character Variant features.
1718        # See section `8.d <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.d>`_.
1719        assert self.cur_token_ == "cvParameters", self.cur_token_
1720        block = self.ast.NestedBlock(
1721            tag, self.cur_token_, location=self.cur_token_location_
1722        )
1723        self.expect_symbol_("{")
1724        for symtab in self.symbol_tables_:
1725            symtab.enter_scope()
1726
1727        statements = block.statements
1728        while self.next_token_ != "}" or self.cur_comments_:
1729            self.advance_lexer_(comments=True)
1730            if self.cur_token_type_ is Lexer.COMMENT:
1731                statements.append(
1732                    self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
1733                )
1734            elif self.is_cur_keyword_(
1735                {
1736                    "FeatUILabelNameID",
1737                    "FeatUITooltipTextNameID",
1738                    "SampleTextNameID",
1739                    "ParamUILabelNameID",
1740                }
1741            ):
1742                statements.append(self.parse_cvNameIDs_(tag, self.cur_token_))
1743            elif self.is_cur_keyword_("Character"):
1744                statements.append(self.parse_cvCharacter_(tag))
1745            elif self.cur_token_ == ";":
1746                continue
1747            else:
1748                raise FeatureLibError(
1749                    "Expected statement: got {} {}".format(
1750                        self.cur_token_type_, self.cur_token_
1751                    ),
1752                    self.cur_token_location_,
1753                )
1754
1755        self.expect_symbol_("}")
1756        for symtab in self.symbol_tables_:
1757            symtab.exit_scope()
1758        self.expect_symbol_(";")
1759        return block
1760
1761    def parse_cvNameIDs_(self, tag, block_name):
1762        assert self.cur_token_ == block_name, self.cur_token_
1763        block = self.ast.NestedBlock(tag, block_name, location=self.cur_token_location_)
1764        self.expect_symbol_("{")
1765        for symtab in self.symbol_tables_:
1766            symtab.enter_scope()
1767        while self.next_token_ != "}" or self.cur_comments_:
1768            self.advance_lexer_(comments=True)
1769            if self.cur_token_type_ is Lexer.COMMENT:
1770                block.statements.append(
1771                    self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
1772                )
1773            elif self.is_cur_keyword_("name"):
1774                location = self.cur_token_location_
1775                platformID, platEncID, langID, string = self.parse_name_()
1776                block.statements.append(
1777                    self.ast.CVParametersNameStatement(
1778                        tag,
1779                        platformID,
1780                        platEncID,
1781                        langID,
1782                        string,
1783                        block_name,
1784                        location=location,
1785                    )
1786                )
1787            elif self.cur_token_ == ";":
1788                continue
1789            else:
1790                raise FeatureLibError('Expected "name"', self.cur_token_location_)
1791        self.expect_symbol_("}")
1792        for symtab in self.symbol_tables_:
1793            symtab.exit_scope()
1794        self.expect_symbol_(";")
1795        return block
1796
1797    def parse_cvCharacter_(self, tag):
1798        assert self.cur_token_ == "Character", self.cur_token_
1799        location, character = self.cur_token_location_, self.expect_any_number_()
1800        self.expect_symbol_(";")
1801        if not (0xFFFFFF >= character >= 0):
1802            raise FeatureLibError(
1803                "Character value must be between "
1804                "{:#x} and {:#x}".format(0, 0xFFFFFF),
1805                location,
1806            )
1807        return self.ast.CharacterStatement(character, tag, location=location)
1808
1809    def parse_FontRevision_(self):
1810        # Parses a ``FontRevision`` statement found in the head table. See
1811        # `section 9.c <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.c>`_.
1812        assert self.cur_token_ == "FontRevision", self.cur_token_
1813        location, version = self.cur_token_location_, self.expect_float_()
1814        self.expect_symbol_(";")
1815        if version <= 0:
1816            raise FeatureLibError("Font revision numbers must be positive", location)
1817        return self.ast.FontRevisionStatement(version, location=location)
1818
1819    def parse_block_(
1820        self, block, vertical, stylisticset=None, size_feature=False, cv_feature=None
1821    ):
1822        self.expect_symbol_("{")
1823        for symtab in self.symbol_tables_:
1824            symtab.enter_scope()
1825
1826        statements = block.statements
1827        while self.next_token_ != "}" or self.cur_comments_:
1828            self.advance_lexer_(comments=True)
1829            if self.cur_token_type_ is Lexer.COMMENT:
1830                statements.append(
1831                    self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
1832                )
1833            elif self.cur_token_type_ is Lexer.GLYPHCLASS:
1834                statements.append(self.parse_glyphclass_definition_())
1835            elif self.is_cur_keyword_("anchorDef"):
1836                statements.append(self.parse_anchordef_())
1837            elif self.is_cur_keyword_({"enum", "enumerate"}):
1838                statements.append(self.parse_enumerate_(vertical=vertical))
1839            elif self.is_cur_keyword_("feature"):
1840                statements.append(self.parse_feature_reference_())
1841            elif self.is_cur_keyword_("ignore"):
1842                statements.append(self.parse_ignore_())
1843            elif self.is_cur_keyword_("language"):
1844                statements.append(self.parse_language_())
1845            elif self.is_cur_keyword_("lookup"):
1846                statements.append(self.parse_lookup_(vertical))
1847            elif self.is_cur_keyword_("lookupflag"):
1848                statements.append(self.parse_lookupflag_())
1849            elif self.is_cur_keyword_("markClass"):
1850                statements.append(self.parse_markClass_())
1851            elif self.is_cur_keyword_({"pos", "position"}):
1852                statements.append(
1853                    self.parse_position_(enumerated=False, vertical=vertical)
1854                )
1855            elif self.is_cur_keyword_("script"):
1856                statements.append(self.parse_script_())
1857            elif self.is_cur_keyword_({"sub", "substitute", "rsub", "reversesub"}):
1858                statements.append(self.parse_substitute_())
1859            elif self.is_cur_keyword_("subtable"):
1860                statements.append(self.parse_subtable_())
1861            elif self.is_cur_keyword_("valueRecordDef"):
1862                statements.append(self.parse_valuerecord_definition_(vertical))
1863            elif stylisticset and self.is_cur_keyword_("featureNames"):
1864                statements.append(self.parse_featureNames_(stylisticset))
1865            elif cv_feature and self.is_cur_keyword_("cvParameters"):
1866                statements.append(self.parse_cvParameters_(cv_feature))
1867            elif size_feature and self.is_cur_keyword_("parameters"):
1868                statements.append(self.parse_size_parameters_())
1869            elif size_feature and self.is_cur_keyword_("sizemenuname"):
1870                statements.append(self.parse_size_menuname_())
1871            elif (
1872                self.cur_token_type_ is Lexer.NAME
1873                and self.cur_token_ in self.extensions
1874            ):
1875                statements.append(self.extensions[self.cur_token_](self))
1876            elif self.cur_token_ == ";":
1877                continue
1878            else:
1879                raise FeatureLibError(
1880                    "Expected glyph class definition or statement: got {} {}".format(
1881                        self.cur_token_type_, self.cur_token_
1882                    ),
1883                    self.cur_token_location_,
1884                )
1885
1886        self.expect_symbol_("}")
1887        for symtab in self.symbol_tables_:
1888            symtab.exit_scope()
1889
1890        name = self.expect_name_()
1891        if name != block.name.strip():
1892            raise FeatureLibError(
1893                'Expected "%s"' % block.name.strip(), self.cur_token_location_
1894            )
1895        self.expect_symbol_(";")
1896
1897        # A multiple substitution may have a single destination, in which case
1898        # it will look just like a single substitution. So if there are both
1899        # multiple and single substitutions, upgrade all the single ones to
1900        # multiple substitutions.
1901
1902        # Check if we have a mix of non-contextual singles and multiples.
1903        has_single = False
1904        has_multiple = False
1905        for s in statements:
1906            if isinstance(s, self.ast.SingleSubstStatement):
1907                has_single = not any([s.prefix, s.suffix, s.forceChain])
1908            elif isinstance(s, self.ast.MultipleSubstStatement):
1909                has_multiple = not any([s.prefix, s.suffix, s.forceChain])
1910
1911        # Upgrade all single substitutions to multiple substitutions.
1912        if has_single and has_multiple:
1913            statements = []
1914            for s in block.statements:
1915                if isinstance(s, self.ast.SingleSubstStatement):
1916                    glyphs = s.glyphs[0].glyphSet()
1917                    replacements = s.replacements[0].glyphSet()
1918                    if len(replacements) == 1:
1919                        replacements *= len(glyphs)
1920                    for i, glyph in enumerate(glyphs):
1921                        statements.append(
1922                            self.ast.MultipleSubstStatement(
1923                                s.prefix,
1924                                glyph,
1925                                s.suffix,
1926                                [replacements[i]],
1927                                s.forceChain,
1928                                location=s.location,
1929                            )
1930                        )
1931                else:
1932                    statements.append(s)
1933            block.statements = statements
1934
1935    def is_cur_keyword_(self, k):
1936        if self.cur_token_type_ is Lexer.NAME:
1937            if isinstance(k, type("")):  # basestring is gone in Python3
1938                return self.cur_token_ == k
1939            else:
1940                return self.cur_token_ in k
1941        return False
1942
1943    def expect_class_name_(self):
1944        self.advance_lexer_()
1945        if self.cur_token_type_ is not Lexer.GLYPHCLASS:
1946            raise FeatureLibError("Expected @NAME", self.cur_token_location_)
1947        return self.cur_token_
1948
1949    def expect_cid_(self):
1950        self.advance_lexer_()
1951        if self.cur_token_type_ is Lexer.CID:
1952            return self.cur_token_
1953        raise FeatureLibError("Expected a CID", self.cur_token_location_)
1954
1955    def expect_filename_(self):
1956        self.advance_lexer_()
1957        if self.cur_token_type_ is not Lexer.FILENAME:
1958            raise FeatureLibError("Expected file name", self.cur_token_location_)
1959        return self.cur_token_
1960
1961    def expect_glyph_(self):
1962        self.advance_lexer_()
1963        if self.cur_token_type_ is Lexer.NAME:
1964            self.cur_token_ = self.cur_token_.lstrip("\\")
1965            if len(self.cur_token_) > 63:
1966                raise FeatureLibError(
1967                    "Glyph names must not be longer than 63 characters",
1968                    self.cur_token_location_,
1969                )
1970            return self.cur_token_
1971        elif self.cur_token_type_ is Lexer.CID:
1972            return "cid%05d" % self.cur_token_
1973        raise FeatureLibError("Expected a glyph name or CID", self.cur_token_location_)
1974
1975    def check_glyph_name_in_glyph_set(self, *names):
1976        """Raises if glyph name (just `start`) or glyph names of a
1977        range (`start` and `end`) are not in the glyph set.
1978
1979        If no glyph set is present, does nothing.
1980        """
1981        if self.glyphNames_:
1982            missing = [name for name in names if name not in self.glyphNames_]
1983            if missing:
1984                raise FeatureLibError(
1985                    "The following glyph names are referenced but are missing from the "
1986                    f"glyph set: {', '.join(missing)}",
1987                    self.cur_token_location_,
1988                )
1989
1990    def expect_markClass_reference_(self):
1991        name = self.expect_class_name_()
1992        mc = self.glyphclasses_.resolve(name)
1993        if mc is None:
1994            raise FeatureLibError(
1995                "Unknown markClass @%s" % name, self.cur_token_location_
1996            )
1997        if not isinstance(mc, self.ast.MarkClass):
1998            raise FeatureLibError(
1999                "@%s is not a markClass" % name, self.cur_token_location_
2000            )
2001        return mc
2002
2003    def expect_tag_(self):
2004        self.advance_lexer_()
2005        if self.cur_token_type_ is not Lexer.NAME:
2006            raise FeatureLibError("Expected a tag", self.cur_token_location_)
2007        if len(self.cur_token_) > 4:
2008            raise FeatureLibError(
2009                "Tags cannot be longer than 4 characters", self.cur_token_location_
2010            )
2011        return (self.cur_token_ + "    ")[:4]
2012
2013    def expect_script_tag_(self):
2014        tag = self.expect_tag_()
2015        if tag == "dflt":
2016            raise FeatureLibError(
2017                '"dflt" is not a valid script tag; use "DFLT" instead',
2018                self.cur_token_location_,
2019            )
2020        return tag
2021
2022    def expect_language_tag_(self):
2023        tag = self.expect_tag_()
2024        if tag == "DFLT":
2025            raise FeatureLibError(
2026                '"DFLT" is not a valid language tag; use "dflt" instead',
2027                self.cur_token_location_,
2028            )
2029        return tag
2030
2031    def expect_symbol_(self, symbol):
2032        self.advance_lexer_()
2033        if self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == symbol:
2034            return symbol
2035        raise FeatureLibError("Expected '%s'" % symbol, self.cur_token_location_)
2036
2037    def expect_keyword_(self, keyword):
2038        self.advance_lexer_()
2039        if self.cur_token_type_ is Lexer.NAME and self.cur_token_ == keyword:
2040            return self.cur_token_
2041        raise FeatureLibError('Expected "%s"' % keyword, self.cur_token_location_)
2042
2043    def expect_name_(self):
2044        self.advance_lexer_()
2045        if self.cur_token_type_ is Lexer.NAME:
2046            return self.cur_token_
2047        raise FeatureLibError("Expected a name", self.cur_token_location_)
2048
2049    def expect_number_(self):
2050        self.advance_lexer_()
2051        if self.cur_token_type_ is Lexer.NUMBER:
2052            return self.cur_token_
2053        raise FeatureLibError("Expected a number", self.cur_token_location_)
2054
2055    def expect_any_number_(self):
2056        self.advance_lexer_()
2057        if self.cur_token_type_ in Lexer.NUMBERS:
2058            return self.cur_token_
2059        raise FeatureLibError(
2060            "Expected a decimal, hexadecimal or octal number", self.cur_token_location_
2061        )
2062
2063    def expect_float_(self):
2064        self.advance_lexer_()
2065        if self.cur_token_type_ is Lexer.FLOAT:
2066            return self.cur_token_
2067        raise FeatureLibError(
2068            "Expected a floating-point number", self.cur_token_location_
2069        )
2070
2071    def expect_decipoint_(self):
2072        if self.next_token_type_ == Lexer.FLOAT:
2073            return self.expect_float_()
2074        elif self.next_token_type_ is Lexer.NUMBER:
2075            return self.expect_number_() / 10
2076        else:
2077            raise FeatureLibError(
2078                "Expected an integer or floating-point number", self.cur_token_location_
2079            )
2080
2081    def expect_stat_flags(self):
2082        value = 0
2083        flags = {
2084            "OlderSiblingFontAttribute": 1,
2085            "ElidableAxisValueName": 2,
2086        }
2087        while self.next_token_ != ";":
2088            if self.next_token_ in flags:
2089                name = self.expect_name_()
2090                value = value | flags[name]
2091            else:
2092                raise FeatureLibError(
2093                    f"Unexpected STAT flag {self.cur_token_}", self.cur_token_location_
2094                )
2095        return value
2096
2097    def expect_stat_values_(self):
2098        if self.next_token_type_ == Lexer.FLOAT:
2099            return self.expect_float_()
2100        elif self.next_token_type_ is Lexer.NUMBER:
2101            return self.expect_number_()
2102        else:
2103            raise FeatureLibError(
2104                "Expected an integer or floating-point number", self.cur_token_location_
2105            )
2106
2107    def expect_string_(self):
2108        self.advance_lexer_()
2109        if self.cur_token_type_ is Lexer.STRING:
2110            return self.cur_token_
2111        raise FeatureLibError("Expected a string", self.cur_token_location_)
2112
2113    def advance_lexer_(self, comments=False):
2114        if comments and self.cur_comments_:
2115            self.cur_token_type_ = Lexer.COMMENT
2116            self.cur_token_, self.cur_token_location_ = self.cur_comments_.pop(0)
2117            return
2118        else:
2119            self.cur_token_type_, self.cur_token_, self.cur_token_location_ = (
2120                self.next_token_type_,
2121                self.next_token_,
2122                self.next_token_location_,
2123            )
2124        while True:
2125            try:
2126                (
2127                    self.next_token_type_,
2128                    self.next_token_,
2129                    self.next_token_location_,
2130                ) = next(self.lexer_)
2131            except StopIteration:
2132                self.next_token_type_, self.next_token_ = (None, None)
2133            if self.next_token_type_ != Lexer.COMMENT:
2134                break
2135            self.cur_comments_.append((self.next_token_, self.next_token_location_))
2136
2137    @staticmethod
2138    def reverse_string_(s):
2139        """'abc' --> 'cba'"""
2140        return "".join(reversed(list(s)))
2141
2142    def make_cid_range_(self, location, start, limit):
2143        """(location, 999, 1001) --> ["cid00999", "cid01000", "cid01001"]"""
2144        result = list()
2145        if start > limit:
2146            raise FeatureLibError(
2147                "Bad range: start should be less than limit", location
2148            )
2149        for cid in range(start, limit + 1):
2150            result.append("cid%05d" % cid)
2151        return result
2152
2153    def make_glyph_range_(self, location, start, limit):
2154        """(location, "a.sc", "d.sc") --> ["a.sc", "b.sc", "c.sc", "d.sc"]"""
2155        result = list()
2156        if len(start) != len(limit):
2157            raise FeatureLibError(
2158                'Bad range: "%s" and "%s" should have the same length' % (start, limit),
2159                location,
2160            )
2161
2162        rev = self.reverse_string_
2163        prefix = os.path.commonprefix([start, limit])
2164        suffix = rev(os.path.commonprefix([rev(start), rev(limit)]))
2165        if len(suffix) > 0:
2166            start_range = start[len(prefix) : -len(suffix)]
2167            limit_range = limit[len(prefix) : -len(suffix)]
2168        else:
2169            start_range = start[len(prefix) :]
2170            limit_range = limit[len(prefix) :]
2171
2172        if start_range >= limit_range:
2173            raise FeatureLibError(
2174                "Start of range must be smaller than its end", location
2175            )
2176
2177        uppercase = re.compile(r"^[A-Z]$")
2178        if uppercase.match(start_range) and uppercase.match(limit_range):
2179            for c in range(ord(start_range), ord(limit_range) + 1):
2180                result.append("%s%c%s" % (prefix, c, suffix))
2181            return result
2182
2183        lowercase = re.compile(r"^[a-z]$")
2184        if lowercase.match(start_range) and lowercase.match(limit_range):
2185            for c in range(ord(start_range), ord(limit_range) + 1):
2186                result.append("%s%c%s" % (prefix, c, suffix))
2187            return result
2188
2189        digits = re.compile(r"^[0-9]{1,3}$")
2190        if digits.match(start_range) and digits.match(limit_range):
2191            for i in range(int(start_range, 10), int(limit_range, 10) + 1):
2192                number = ("000" + str(i))[-len(start_range) :]
2193                result.append("%s%s%s" % (prefix, number, suffix))
2194            return result
2195
2196        raise FeatureLibError('Bad range: "%s-%s"' % (start, limit), location)
2197
2198
2199class SymbolTable(object):
2200    def __init__(self):
2201        self.scopes_ = [{}]
2202
2203    def enter_scope(self):
2204        self.scopes_.append({})
2205
2206    def exit_scope(self):
2207        self.scopes_.pop()
2208
2209    def define(self, name, item):
2210        self.scopes_[-1][name] = item
2211
2212    def resolve(self, name):
2213        for scope in reversed(self.scopes_):
2214            item = scope.get(name)
2215            if item:
2216                return item
2217        return None
2218