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