2380 lines
		
	
	
		
			97 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			2380 lines
		
	
	
		
			97 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from fontTools.feaLib.error import FeatureLibError
 | |
| from fontTools.feaLib.lexer import Lexer, IncludingLexer, NonIncludingLexer
 | |
| from fontTools.feaLib.variableScalar import VariableScalar
 | |
| from fontTools.misc.encodingTools import getEncoding
 | |
| from fontTools.misc.textTools import bytechr, tobytes, tostr
 | |
| import fontTools.feaLib.ast as ast
 | |
| import logging
 | |
| import os
 | |
| import re
 | |
| 
 | |
| 
 | |
| log = logging.getLogger(__name__)
 | |
| 
 | |
| 
 | |
| class Parser(object):
 | |
|     """Initializes a Parser object.
 | |
| 
 | |
|     Example:
 | |
| 
 | |
|         .. code:: python
 | |
| 
 | |
|             from fontTools.feaLib.parser import Parser
 | |
|             parser = Parser(file, font.getReverseGlyphMap())
 | |
|             parsetree = parser.parse()
 | |
| 
 | |
|     Note: the ``glyphNames`` iterable serves a double role to help distinguish
 | |
|     glyph names from ranges in the presence of hyphens and to ensure that glyph
 | |
|     names referenced in a feature file are actually part of a font's glyph set.
 | |
|     If the iterable is left empty, no glyph name in glyph set checking takes
 | |
|     place, and all glyph tokens containing hyphens are treated as literal glyph
 | |
|     names, not as ranges. (Adding a space around the hyphen can, in any case,
 | |
|     help to disambiguate ranges from glyph names containing hyphens.)
 | |
| 
 | |
|     By default, the parser will follow ``include()`` statements in the feature
 | |
|     file. To turn this off, pass ``followIncludes=False``. Pass a directory string as
 | |
|     ``includeDir`` to explicitly declare a directory to search included feature files
 | |
|     in.
 | |
|     """
 | |
| 
 | |
|     extensions = {}
 | |
|     ast = ast
 | |
|     SS_FEATURE_TAGS = {"ss%02d" % i for i in range(1, 20 + 1)}
 | |
|     CV_FEATURE_TAGS = {"cv%02d" % i for i in range(1, 99 + 1)}
 | |
| 
 | |
|     def __init__(
 | |
|         self, featurefile, glyphNames=(), followIncludes=True, includeDir=None, **kwargs
 | |
|     ):
 | |
|         if "glyphMap" in kwargs:
 | |
|             from fontTools.misc.loggingTools import deprecateArgument
 | |
| 
 | |
|             deprecateArgument("glyphMap", "use 'glyphNames' (iterable) instead")
 | |
|             if glyphNames:
 | |
|                 raise TypeError(
 | |
|                     "'glyphNames' and (deprecated) 'glyphMap' are " "mutually exclusive"
 | |
|                 )
 | |
|             glyphNames = kwargs.pop("glyphMap")
 | |
|         if kwargs:
 | |
|             raise TypeError(
 | |
|                 "unsupported keyword argument%s: %s"
 | |
|                 % ("" if len(kwargs) == 1 else "s", ", ".join(repr(k) for k in kwargs))
 | |
|             )
 | |
| 
 | |
|         self.glyphNames_ = set(glyphNames)
 | |
|         self.doc_ = self.ast.FeatureFile()
 | |
|         self.anchors_ = SymbolTable()
 | |
|         self.glyphclasses_ = SymbolTable()
 | |
|         self.lookups_ = SymbolTable()
 | |
|         self.valuerecords_ = SymbolTable()
 | |
|         self.symbol_tables_ = {self.anchors_, self.valuerecords_}
 | |
|         self.next_token_type_, self.next_token_ = (None, None)
 | |
|         self.cur_comments_ = []
 | |
|         self.next_token_location_ = None
 | |
|         lexerClass = IncludingLexer if followIncludes else NonIncludingLexer
 | |
|         self.lexer_ = lexerClass(featurefile, includeDir=includeDir)
 | |
|         self.missing = {}
 | |
|         self.advance_lexer_(comments=True)
 | |
| 
 | |
|     def parse(self):
 | |
|         """Parse the file, and return a :class:`fontTools.feaLib.ast.FeatureFile`
 | |
|         object representing the root of the abstract syntax tree containing the
 | |
|         parsed contents of the file."""
 | |
|         statements = self.doc_.statements
 | |
|         while self.next_token_type_ is not None or self.cur_comments_:
 | |
|             self.advance_lexer_(comments=True)
 | |
|             if self.cur_token_type_ is Lexer.COMMENT:
 | |
|                 statements.append(
 | |
|                     self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
 | |
|                 )
 | |
|             elif self.is_cur_keyword_("include"):
 | |
|                 statements.append(self.parse_include_())
 | |
|             elif self.cur_token_type_ is Lexer.GLYPHCLASS:
 | |
|                 statements.append(self.parse_glyphclass_definition_())
 | |
|             elif self.is_cur_keyword_(("anon", "anonymous")):
 | |
|                 statements.append(self.parse_anonymous_())
 | |
|             elif self.is_cur_keyword_("anchorDef"):
 | |
|                 statements.append(self.parse_anchordef_())
 | |
|             elif self.is_cur_keyword_("languagesystem"):
 | |
|                 statements.append(self.parse_languagesystem_())
 | |
|             elif self.is_cur_keyword_("lookup"):
 | |
|                 statements.append(self.parse_lookup_(vertical=False))
 | |
|             elif self.is_cur_keyword_("markClass"):
 | |
|                 statements.append(self.parse_markClass_())
 | |
|             elif self.is_cur_keyword_("feature"):
 | |
|                 statements.append(self.parse_feature_block_())
 | |
|             elif self.is_cur_keyword_("conditionset"):
 | |
|                 statements.append(self.parse_conditionset_())
 | |
|             elif self.is_cur_keyword_("variation"):
 | |
|                 statements.append(self.parse_feature_block_(variation=True))
 | |
|             elif self.is_cur_keyword_("table"):
 | |
|                 statements.append(self.parse_table_())
 | |
|             elif self.is_cur_keyword_("valueRecordDef"):
 | |
|                 statements.append(self.parse_valuerecord_definition_(vertical=False))
 | |
|             elif (
 | |
|                 self.cur_token_type_ is Lexer.NAME
 | |
|                 and self.cur_token_ in self.extensions
 | |
|             ):
 | |
|                 statements.append(self.extensions[self.cur_token_](self))
 | |
|             elif self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == ";":
 | |
|                 continue
 | |
|             else:
 | |
|                 raise FeatureLibError(
 | |
|                     "Expected feature, languagesystem, lookup, markClass, "
 | |
|                     'table, or glyph class definition, got {} "{}"'.format(
 | |
|                         self.cur_token_type_, self.cur_token_
 | |
|                     ),
 | |
|                     self.cur_token_location_,
 | |
|                 )
 | |
|         # Report any missing glyphs at the end of parsing
 | |
|         if self.missing:
 | |
|             error = [
 | |
|                 " %s (first found at %s)" % (name, loc)
 | |
|                 for name, loc in self.missing.items()
 | |
|             ]
 | |
|             raise FeatureLibError(
 | |
|                 "The following glyph names are referenced but are missing from the "
 | |
|                 "glyph set:\n" + ("\n".join(error)),
 | |
|                 None,
 | |
|             )
 | |
|         return self.doc_
 | |
| 
 | |
|     def parse_anchor_(self):
 | |
|         # Parses an anchor in any of the four formats given in the feature
 | |
|         # file specification (2.e.vii).
 | |
|         self.expect_symbol_("<")
 | |
|         self.expect_keyword_("anchor")
 | |
|         location = self.cur_token_location_
 | |
| 
 | |
|         if self.next_token_ == "NULL":  # Format D
 | |
|             self.expect_keyword_("NULL")
 | |
|             self.expect_symbol_(">")
 | |
|             return None
 | |
| 
 | |
|         if self.next_token_type_ == Lexer.NAME:  # Format E
 | |
|             name = self.expect_name_()
 | |
|             anchordef = self.anchors_.resolve(name)
 | |
|             if anchordef is None:
 | |
|                 raise FeatureLibError(
 | |
|                     'Unknown anchor "%s"' % name, self.cur_token_location_
 | |
|                 )
 | |
|             self.expect_symbol_(">")
 | |
|             return self.ast.Anchor(
 | |
|                 anchordef.x,
 | |
|                 anchordef.y,
 | |
|                 name=name,
 | |
|                 contourpoint=anchordef.contourpoint,
 | |
|                 xDeviceTable=None,
 | |
|                 yDeviceTable=None,
 | |
|                 location=location,
 | |
|             )
 | |
| 
 | |
|         x, y = self.expect_number_(variable=True), self.expect_number_(variable=True)
 | |
| 
 | |
|         contourpoint = None
 | |
|         if self.next_token_ == "contourpoint":  # Format B
 | |
|             self.expect_keyword_("contourpoint")
 | |
|             contourpoint = self.expect_number_()
 | |
| 
 | |
|         if self.next_token_ == "<":  # Format C
 | |
|             xDeviceTable = self.parse_device_()
 | |
|             yDeviceTable = self.parse_device_()
 | |
|         else:
 | |
|             xDeviceTable, yDeviceTable = None, None
 | |
| 
 | |
|         self.expect_symbol_(">")
 | |
|         return self.ast.Anchor(
 | |
|             x,
 | |
|             y,
 | |
|             name=None,
 | |
|             contourpoint=contourpoint,
 | |
|             xDeviceTable=xDeviceTable,
 | |
|             yDeviceTable=yDeviceTable,
 | |
|             location=location,
 | |
|         )
 | |
| 
 | |
|     def parse_anchor_marks_(self):
 | |
|         # Parses a sequence of ``[<anchor> mark @MARKCLASS]*.``
 | |
|         anchorMarks = []  # [(self.ast.Anchor, markClassName)*]
 | |
|         while self.next_token_ == "<":
 | |
|             anchor = self.parse_anchor_()
 | |
|             if anchor is None and self.next_token_ != "mark":
 | |
|                 continue  # <anchor NULL> without mark, eg. in GPOS type 5
 | |
|             self.expect_keyword_("mark")
 | |
|             markClass = self.expect_markClass_reference_()
 | |
|             anchorMarks.append((anchor, markClass))
 | |
|         return anchorMarks
 | |
| 
 | |
|     def parse_anchordef_(self):
 | |
|         # Parses a named anchor definition (`section 2.e.viii <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#2.e.vii>`_).
 | |
|         assert self.is_cur_keyword_("anchorDef")
 | |
|         location = self.cur_token_location_
 | |
|         x, y = self.expect_number_(), self.expect_number_()
 | |
|         contourpoint = None
 | |
|         if self.next_token_ == "contourpoint":
 | |
|             self.expect_keyword_("contourpoint")
 | |
|             contourpoint = self.expect_number_()
 | |
|         name = self.expect_name_()
 | |
|         self.expect_symbol_(";")
 | |
|         anchordef = self.ast.AnchorDefinition(
 | |
|             name, x, y, contourpoint=contourpoint, location=location
 | |
|         )
 | |
|         self.anchors_.define(name, anchordef)
 | |
|         return anchordef
 | |
| 
 | |
|     def parse_anonymous_(self):
 | |
|         # Parses an anonymous data block (`section 10 <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#10>`_).
 | |
|         assert self.is_cur_keyword_(("anon", "anonymous"))
 | |
|         tag = self.expect_tag_()
 | |
|         _, content, location = self.lexer_.scan_anonymous_block(tag)
 | |
|         self.advance_lexer_()
 | |
|         self.expect_symbol_("}")
 | |
|         end_tag = self.expect_tag_()
 | |
|         assert tag == end_tag, "bad splitting in Lexer.scan_anonymous_block()"
 | |
|         self.expect_symbol_(";")
 | |
|         return self.ast.AnonymousBlock(tag, content, location=location)
 | |
| 
 | |
|     def parse_attach_(self):
 | |
|         # Parses a GDEF Attach statement (`section 9.b <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.b>`_)
 | |
|         assert self.is_cur_keyword_("Attach")
 | |
|         location = self.cur_token_location_
 | |
|         glyphs = self.parse_glyphclass_(accept_glyphname=True)
 | |
|         contourPoints = {self.expect_number_()}
 | |
|         while self.next_token_ != ";":
 | |
|             contourPoints.add(self.expect_number_())
 | |
|         self.expect_symbol_(";")
 | |
|         return self.ast.AttachStatement(glyphs, contourPoints, location=location)
 | |
| 
 | |
|     def parse_enumerate_(self, vertical):
 | |
|         # Parse an enumerated pair positioning rule (`section 6.b.ii <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#6.b.ii>`_).
 | |
|         assert self.cur_token_ in {"enumerate", "enum"}
 | |
|         self.advance_lexer_()
 | |
|         return self.parse_position_(enumerated=True, vertical=vertical)
 | |
| 
 | |
|     def parse_GlyphClassDef_(self):
 | |
|         # Parses 'GlyphClassDef @BASE, @LIGATURES, @MARKS, @COMPONENTS;'
 | |
|         assert self.is_cur_keyword_("GlyphClassDef")
 | |
|         location = self.cur_token_location_
 | |
|         if self.next_token_ != ",":
 | |
|             baseGlyphs = self.parse_glyphclass_(accept_glyphname=False)
 | |
|         else:
 | |
|             baseGlyphs = None
 | |
|         self.expect_symbol_(",")
 | |
|         if self.next_token_ != ",":
 | |
|             ligatureGlyphs = self.parse_glyphclass_(accept_glyphname=False)
 | |
|         else:
 | |
|             ligatureGlyphs = None
 | |
|         self.expect_symbol_(",")
 | |
|         if self.next_token_ != ",":
 | |
|             markGlyphs = self.parse_glyphclass_(accept_glyphname=False)
 | |
|         else:
 | |
|             markGlyphs = None
 | |
|         self.expect_symbol_(",")
 | |
|         if self.next_token_ != ";":
 | |
|             componentGlyphs = self.parse_glyphclass_(accept_glyphname=False)
 | |
|         else:
 | |
|             componentGlyphs = None
 | |
|         self.expect_symbol_(";")
 | |
|         return self.ast.GlyphClassDefStatement(
 | |
|             baseGlyphs, markGlyphs, ligatureGlyphs, componentGlyphs, location=location
 | |
|         )
 | |
| 
 | |
|     def parse_glyphclass_definition_(self):
 | |
|         # Parses glyph class definitions such as '@UPPERCASE = [A-Z];'
 | |
|         location, name = self.cur_token_location_, self.cur_token_
 | |
|         self.expect_symbol_("=")
 | |
|         glyphs = self.parse_glyphclass_(accept_glyphname=False)
 | |
|         self.expect_symbol_(";")
 | |
|         glyphclass = self.ast.GlyphClassDefinition(name, glyphs, location=location)
 | |
|         self.glyphclasses_.define(name, glyphclass)
 | |
|         return glyphclass
 | |
| 
 | |
|     def split_glyph_range_(self, name, location):
 | |
|         # Since v1.20, the OpenType Feature File specification allows
 | |
|         # for dashes in glyph names. A sequence like "a-b-c-d" could
 | |
|         # therefore mean a single glyph whose name happens to be
 | |
|         # "a-b-c-d", or it could mean a range from glyph "a" to glyph
 | |
|         # "b-c-d", or a range from glyph "a-b" to glyph "c-d", or a
 | |
|         # range from glyph "a-b-c" to glyph "d".Technically, this
 | |
|         # example could be resolved because the (pretty complex)
 | |
|         # definition of glyph ranges renders most of these splits
 | |
|         # invalid. But the specification does not say that a compiler
 | |
|         # should try to apply such fancy heuristics. To encourage
 | |
|         # unambiguous feature files, we therefore try all possible
 | |
|         # splits and reject the feature file if there are multiple
 | |
|         # splits possible. It is intentional that we don't just emit a
 | |
|         # warning; warnings tend to get ignored. To fix the problem,
 | |
|         # font designers can trivially add spaces around the intended
 | |
|         # split point, and we emit a compiler error that suggests
 | |
|         # how exactly the source should be rewritten to make things
 | |
|         # unambiguous.
 | |
|         parts = name.split("-")
 | |
|         solutions = []
 | |
|         for i in range(len(parts)):
 | |
|             start, limit = "-".join(parts[0:i]), "-".join(parts[i:])
 | |
|             if start in self.glyphNames_ and limit in self.glyphNames_:
 | |
|                 solutions.append((start, limit))
 | |
|         if len(solutions) == 1:
 | |
|             start, limit = solutions[0]
 | |
|             return start, limit
 | |
|         elif len(solutions) == 0:
 | |
|             raise FeatureLibError(
 | |
|                 '"%s" is not a glyph in the font, and it can not be split '
 | |
|                 "into a range of known glyphs" % name,
 | |
|                 location,
 | |
|             )
 | |
|         else:
 | |
|             ranges = " or ".join(['"%s - %s"' % (s, l) for s, l in solutions])
 | |
|             raise FeatureLibError(
 | |
|                 'Ambiguous glyph range "%s"; '
 | |
|                 "please use %s to clarify what you mean" % (name, ranges),
 | |
|                 location,
 | |
|             )
 | |
| 
 | |
|     def parse_glyphclass_(self, accept_glyphname, accept_null=False):
 | |
|         # Parses a glyph class, either named or anonymous, or (if
 | |
|         # ``bool(accept_glyphname)``) a glyph name. If ``bool(accept_null)`` then
 | |
|         # also accept the special NULL glyph.
 | |
|         if accept_glyphname and self.next_token_type_ in (Lexer.NAME, Lexer.CID):
 | |
|             if accept_null and self.next_token_ == "NULL":
 | |
|                 # If you want a glyph called NULL, you should escape it.
 | |
|                 self.advance_lexer_()
 | |
|                 return self.ast.NullGlyph(location=self.cur_token_location_)
 | |
|             glyph = self.expect_glyph_()
 | |
|             self.check_glyph_name_in_glyph_set(glyph)
 | |
|             return self.ast.GlyphName(glyph, location=self.cur_token_location_)
 | |
|         if self.next_token_type_ is Lexer.GLYPHCLASS:
 | |
|             self.advance_lexer_()
 | |
|             gc = self.glyphclasses_.resolve(self.cur_token_)
 | |
|             if gc is None:
 | |
|                 raise FeatureLibError(
 | |
|                     "Unknown glyph class @%s" % self.cur_token_,
 | |
|                     self.cur_token_location_,
 | |
|                 )
 | |
|             if isinstance(gc, self.ast.MarkClass):
 | |
|                 return self.ast.MarkClassName(gc, location=self.cur_token_location_)
 | |
|             else:
 | |
|                 return self.ast.GlyphClassName(gc, location=self.cur_token_location_)
 | |
| 
 | |
|         self.expect_symbol_("[")
 | |
|         location = self.cur_token_location_
 | |
|         glyphs = self.ast.GlyphClass(location=location)
 | |
|         while self.next_token_ != "]":
 | |
|             if self.next_token_type_ is Lexer.NAME:
 | |
|                 glyph = self.expect_glyph_()
 | |
|                 location = self.cur_token_location_
 | |
|                 if "-" in glyph and self.glyphNames_ and glyph not in self.glyphNames_:
 | |
|                     start, limit = self.split_glyph_range_(glyph, location)
 | |
|                     self.check_glyph_name_in_glyph_set(start, limit)
 | |
|                     glyphs.add_range(
 | |
|                         start, limit, self.make_glyph_range_(location, start, limit)
 | |
|                     )
 | |
|                 elif self.next_token_ == "-":
 | |
|                     start = glyph
 | |
|                     self.expect_symbol_("-")
 | |
|                     limit = self.expect_glyph_()
 | |
|                     self.check_glyph_name_in_glyph_set(start, limit)
 | |
|                     glyphs.add_range(
 | |
|                         start, limit, self.make_glyph_range_(location, start, limit)
 | |
|                     )
 | |
|                 else:
 | |
|                     if "-" in glyph and not self.glyphNames_:
 | |
|                         log.warning(
 | |
|                             str(
 | |
|                                 FeatureLibError(
 | |
|                                     f"Ambiguous glyph name that looks like a range: {glyph!r}",
 | |
|                                     location,
 | |
|                                 )
 | |
|                             )
 | |
|                         )
 | |
|                     self.check_glyph_name_in_glyph_set(glyph)
 | |
|                     glyphs.append(glyph)
 | |
|             elif self.next_token_type_ is Lexer.CID:
 | |
|                 glyph = self.expect_glyph_()
 | |
|                 if self.next_token_ == "-":
 | |
|                     range_location = self.cur_token_location_
 | |
|                     range_start = self.cur_token_
 | |
|                     self.expect_symbol_("-")
 | |
|                     range_end = self.expect_cid_()
 | |
|                     self.check_glyph_name_in_glyph_set(
 | |
|                         f"cid{range_start:05d}",
 | |
|                         f"cid{range_end:05d}",
 | |
|                     )
 | |
|                     glyphs.add_cid_range(
 | |
|                         range_start,
 | |
|                         range_end,
 | |
|                         self.make_cid_range_(range_location, range_start, range_end),
 | |
|                     )
 | |
|                 else:
 | |
|                     glyph_name = f"cid{self.cur_token_:05d}"
 | |
|                     self.check_glyph_name_in_glyph_set(glyph_name)
 | |
|                     glyphs.append(glyph_name)
 | |
|             elif self.next_token_type_ is Lexer.GLYPHCLASS:
 | |
|                 self.advance_lexer_()
 | |
|                 gc = self.glyphclasses_.resolve(self.cur_token_)
 | |
|                 if gc is None:
 | |
|                     raise FeatureLibError(
 | |
|                         "Unknown glyph class @%s" % self.cur_token_,
 | |
|                         self.cur_token_location_,
 | |
|                     )
 | |
|                 if isinstance(gc, self.ast.MarkClass):
 | |
|                     gc = self.ast.MarkClassName(gc, location=self.cur_token_location_)
 | |
|                 else:
 | |
|                     gc = self.ast.GlyphClassName(gc, location=self.cur_token_location_)
 | |
|                 glyphs.add_class(gc)
 | |
|             else:
 | |
|                 raise FeatureLibError(
 | |
|                     "Expected glyph name, glyph range, "
 | |
|                     f"or glyph class reference, found {self.next_token_!r}",
 | |
|                     self.next_token_location_,
 | |
|                 )
 | |
|         self.expect_symbol_("]")
 | |
|         return glyphs
 | |
| 
 | |
|     def parse_glyph_pattern_(self, vertical):
 | |
|         # Parses a glyph pattern, including lookups and context, e.g.::
 | |
|         #
 | |
|         #    a b
 | |
|         #    a b c' d e
 | |
|         #    a b c' lookup ChangeC d e
 | |
|         prefix, glyphs, lookups, values, suffix = ([], [], [], [], [])
 | |
|         hasMarks = False
 | |
|         while self.next_token_ not in {"by", "from", ";", ","}:
 | |
|             gc = self.parse_glyphclass_(accept_glyphname=True)
 | |
|             marked = False
 | |
|             if self.next_token_ == "'":
 | |
|                 self.expect_symbol_("'")
 | |
|                 hasMarks = marked = True
 | |
|             if marked:
 | |
|                 if suffix:
 | |
|                     # makeotf also reports this as an error, while FontForge
 | |
|                     # silently inserts ' in all the intervening glyphs.
 | |
|                     # https://github.com/fonttools/fonttools/pull/1096
 | |
|                     raise FeatureLibError(
 | |
|                         "Unsupported contextual target sequence: at most "
 | |
|                         "one run of marked (') glyph/class names allowed",
 | |
|                         self.cur_token_location_,
 | |
|                     )
 | |
|                 glyphs.append(gc)
 | |
|             elif glyphs:
 | |
|                 suffix.append(gc)
 | |
|             else:
 | |
|                 prefix.append(gc)
 | |
| 
 | |
|             if self.is_next_value_():
 | |
|                 values.append(self.parse_valuerecord_(vertical))
 | |
|             else:
 | |
|                 values.append(None)
 | |
| 
 | |
|             lookuplist = None
 | |
|             while self.next_token_ == "lookup":
 | |
|                 if lookuplist is None:
 | |
|                     lookuplist = []
 | |
|                 self.expect_keyword_("lookup")
 | |
|                 if not marked:
 | |
|                     raise FeatureLibError(
 | |
|                         "Lookups can only follow marked glyphs",
 | |
|                         self.cur_token_location_,
 | |
|                     )
 | |
|                 lookup_name = self.expect_name_()
 | |
|                 lookup = self.lookups_.resolve(lookup_name)
 | |
|                 if lookup is None:
 | |
|                     raise FeatureLibError(
 | |
|                         'Unknown lookup "%s"' % lookup_name, self.cur_token_location_
 | |
|                     )
 | |
|                 lookuplist.append(lookup)
 | |
|             if marked:
 | |
|                 lookups.append(lookuplist)
 | |
| 
 | |
|         if not glyphs and not suffix:  # eg., "sub f f i by"
 | |
|             assert lookups == []
 | |
|             return ([], prefix, [None] * len(prefix), values, [], hasMarks)
 | |
|         else:
 | |
|             if any(values[: len(prefix)]):
 | |
|                 raise FeatureLibError(
 | |
|                     "Positioning cannot be applied in the bactrack glyph sequence, "
 | |
|                     "before the marked glyph sequence.",
 | |
|                     self.cur_token_location_,
 | |
|                 )
 | |
|             marked_values = values[len(prefix) : len(prefix) + len(glyphs)]
 | |
|             if any(marked_values):
 | |
|                 if any(values[len(prefix) + len(glyphs) :]):
 | |
|                     raise FeatureLibError(
 | |
|                         "Positioning values are allowed only in the marked glyph "
 | |
|                         "sequence, or after the final glyph node when only one glyph "
 | |
|                         "node is marked.",
 | |
|                         self.cur_token_location_,
 | |
|                     )
 | |
|                 values = marked_values
 | |
|             elif values and values[-1]:
 | |
|                 if len(glyphs) > 1 or any(values[:-1]):
 | |
|                     raise FeatureLibError(
 | |
|                         "Positioning values are allowed only in the marked glyph "
 | |
|                         "sequence, or after the final glyph node when only one glyph "
 | |
|                         "node is marked.",
 | |
|                         self.cur_token_location_,
 | |
|                     )
 | |
|                 values = values[-1:]
 | |
|             elif any(values):
 | |
|                 raise FeatureLibError(
 | |
|                     "Positioning values are allowed only in the marked glyph "
 | |
|                     "sequence, or after the final glyph node when only one glyph "
 | |
|                     "node is marked.",
 | |
|                     self.cur_token_location_,
 | |
|                 )
 | |
|             return (prefix, glyphs, lookups, values, suffix, hasMarks)
 | |
| 
 | |
|     def parse_ignore_glyph_pattern_(self, sub):
 | |
|         location = self.cur_token_location_
 | |
|         prefix, glyphs, lookups, values, suffix, hasMarks = self.parse_glyph_pattern_(
 | |
|             vertical=False
 | |
|         )
 | |
|         if any(lookups):
 | |
|             raise FeatureLibError(
 | |
|                 f'No lookups can be specified for "ignore {sub}"', location
 | |
|             )
 | |
|         if not hasMarks:
 | |
|             error = FeatureLibError(
 | |
|                 f'Ambiguous "ignore {sub}", there should be least one marked glyph',
 | |
|                 location,
 | |
|             )
 | |
|             log.warning(str(error))
 | |
|             suffix, glyphs = glyphs[1:], glyphs[0:1]
 | |
|         chainContext = (prefix, glyphs, suffix)
 | |
|         return chainContext
 | |
| 
 | |
|     def parse_ignore_context_(self, sub):
 | |
|         location = self.cur_token_location_
 | |
|         chainContext = [self.parse_ignore_glyph_pattern_(sub)]
 | |
|         while self.next_token_ == ",":
 | |
|             self.expect_symbol_(",")
 | |
|             chainContext.append(self.parse_ignore_glyph_pattern_(sub))
 | |
|         self.expect_symbol_(";")
 | |
|         return chainContext
 | |
| 
 | |
|     def parse_ignore_(self):
 | |
|         # Parses an ignore sub/pos rule.
 | |
|         assert self.is_cur_keyword_("ignore")
 | |
|         location = self.cur_token_location_
 | |
|         self.advance_lexer_()
 | |
|         if self.cur_token_ in ["substitute", "sub"]:
 | |
|             chainContext = self.parse_ignore_context_("sub")
 | |
|             return self.ast.IgnoreSubstStatement(chainContext, location=location)
 | |
|         if self.cur_token_ in ["position", "pos"]:
 | |
|             chainContext = self.parse_ignore_context_("pos")
 | |
|             return self.ast.IgnorePosStatement(chainContext, location=location)
 | |
|         raise FeatureLibError(
 | |
|             'Expected "substitute" or "position"', self.cur_token_location_
 | |
|         )
 | |
| 
 | |
|     def parse_include_(self):
 | |
|         assert self.cur_token_ == "include"
 | |
|         location = self.cur_token_location_
 | |
|         filename = self.expect_filename_()
 | |
|         # self.expect_symbol_(";")
 | |
|         return ast.IncludeStatement(filename, location=location)
 | |
| 
 | |
|     def parse_language_(self):
 | |
|         assert self.is_cur_keyword_("language")
 | |
|         location = self.cur_token_location_
 | |
|         language = self.expect_language_tag_()
 | |
|         include_default, required = (True, False)
 | |
|         if self.next_token_ in {"exclude_dflt", "include_dflt"}:
 | |
|             include_default = self.expect_name_() == "include_dflt"
 | |
|         if self.next_token_ == "required":
 | |
|             self.expect_keyword_("required")
 | |
|             required = True
 | |
|         self.expect_symbol_(";")
 | |
|         return self.ast.LanguageStatement(
 | |
|             language, include_default, required, location=location
 | |
|         )
 | |
| 
 | |
|     def parse_ligatureCaretByIndex_(self):
 | |
|         assert self.is_cur_keyword_("LigatureCaretByIndex")
 | |
|         location = self.cur_token_location_
 | |
|         glyphs = self.parse_glyphclass_(accept_glyphname=True)
 | |
|         carets = [self.expect_number_()]
 | |
|         while self.next_token_ != ";":
 | |
|             carets.append(self.expect_number_())
 | |
|         self.expect_symbol_(";")
 | |
|         return self.ast.LigatureCaretByIndexStatement(glyphs, carets, location=location)
 | |
| 
 | |
|     def parse_ligatureCaretByPos_(self):
 | |
|         assert self.is_cur_keyword_("LigatureCaretByPos")
 | |
|         location = self.cur_token_location_
 | |
|         glyphs = self.parse_glyphclass_(accept_glyphname=True)
 | |
|         carets = [self.expect_number_(variable=True)]
 | |
|         while self.next_token_ != ";":
 | |
|             carets.append(self.expect_number_(variable=True))
 | |
|         self.expect_symbol_(";")
 | |
|         return self.ast.LigatureCaretByPosStatement(glyphs, carets, location=location)
 | |
| 
 | |
|     def parse_lookup_(self, vertical):
 | |
|         # Parses a ``lookup`` - either a lookup block, or a lookup reference
 | |
|         # inside a feature.
 | |
|         assert self.is_cur_keyword_("lookup")
 | |
|         location, name = self.cur_token_location_, self.expect_name_()
 | |
| 
 | |
|         if self.next_token_ == ";":
 | |
|             lookup = self.lookups_.resolve(name)
 | |
|             if lookup is None:
 | |
|                 raise FeatureLibError(
 | |
|                     'Unknown lookup "%s"' % name, self.cur_token_location_
 | |
|                 )
 | |
|             self.expect_symbol_(";")
 | |
|             return self.ast.LookupReferenceStatement(lookup, location=location)
 | |
| 
 | |
|         use_extension = False
 | |
|         if self.next_token_ == "useExtension":
 | |
|             self.expect_keyword_("useExtension")
 | |
|             use_extension = True
 | |
| 
 | |
|         block = self.ast.LookupBlock(name, use_extension, location=location)
 | |
|         self.parse_block_(block, vertical)
 | |
|         self.lookups_.define(name, block)
 | |
|         return block
 | |
| 
 | |
|     def parse_lookupflag_(self):
 | |
|         # Parses a ``lookupflag`` statement, either specified by number or
 | |
|         # in words.
 | |
|         assert self.is_cur_keyword_("lookupflag")
 | |
|         location = self.cur_token_location_
 | |
| 
 | |
|         # format B: "lookupflag 6;"
 | |
|         if self.next_token_type_ == Lexer.NUMBER:
 | |
|             value = self.expect_number_()
 | |
|             self.expect_symbol_(";")
 | |
|             return self.ast.LookupFlagStatement(value, location=location)
 | |
| 
 | |
|         # format A: "lookupflag RightToLeft MarkAttachmentType @M;"
 | |
|         value_seen = False
 | |
|         value, markAttachment, markFilteringSet = 0, None, None
 | |
|         flags = {
 | |
|             "RightToLeft": 1,
 | |
|             "IgnoreBaseGlyphs": 2,
 | |
|             "IgnoreLigatures": 4,
 | |
|             "IgnoreMarks": 8,
 | |
|         }
 | |
|         seen = set()
 | |
|         while self.next_token_ != ";":
 | |
|             if self.next_token_ in seen:
 | |
|                 raise FeatureLibError(
 | |
|                     "%s can be specified only once" % self.next_token_,
 | |
|                     self.next_token_location_,
 | |
|                 )
 | |
|             seen.add(self.next_token_)
 | |
|             if self.next_token_ == "MarkAttachmentType":
 | |
|                 self.expect_keyword_("MarkAttachmentType")
 | |
|                 markAttachment = self.parse_glyphclass_(accept_glyphname=False)
 | |
|             elif self.next_token_ == "UseMarkFilteringSet":
 | |
|                 self.expect_keyword_("UseMarkFilteringSet")
 | |
|                 markFilteringSet = self.parse_glyphclass_(accept_glyphname=False)
 | |
|             elif self.next_token_ in flags:
 | |
|                 value_seen = True
 | |
|                 value = value | flags[self.expect_name_()]
 | |
|             else:
 | |
|                 raise FeatureLibError(
 | |
|                     '"%s" is not a recognized lookupflag' % self.next_token_,
 | |
|                     self.next_token_location_,
 | |
|                 )
 | |
|         self.expect_symbol_(";")
 | |
| 
 | |
|         if not any([value_seen, markAttachment, markFilteringSet]):
 | |
|             raise FeatureLibError(
 | |
|                 "lookupflag must have a value", self.next_token_location_
 | |
|             )
 | |
| 
 | |
|         return self.ast.LookupFlagStatement(
 | |
|             value,
 | |
|             markAttachment=markAttachment,
 | |
|             markFilteringSet=markFilteringSet,
 | |
|             location=location,
 | |
|         )
 | |
| 
 | |
|     def parse_markClass_(self):
 | |
|         assert self.is_cur_keyword_("markClass")
 | |
|         location = self.cur_token_location_
 | |
|         glyphs = self.parse_glyphclass_(accept_glyphname=True)
 | |
|         if not glyphs.glyphSet():
 | |
|             raise FeatureLibError(
 | |
|                 "Empty glyph class in mark class definition", location
 | |
|             )
 | |
|         anchor = self.parse_anchor_()
 | |
|         name = self.expect_class_name_()
 | |
|         self.expect_symbol_(";")
 | |
|         markClass = self.doc_.markClasses.get(name)
 | |
|         if markClass is None:
 | |
|             markClass = self.ast.MarkClass(name)
 | |
|             self.doc_.markClasses[name] = markClass
 | |
|             self.glyphclasses_.define(name, markClass)
 | |
|         mcdef = self.ast.MarkClassDefinition(
 | |
|             markClass, anchor, glyphs, location=location
 | |
|         )
 | |
|         markClass.addDefinition(mcdef)
 | |
|         return mcdef
 | |
| 
 | |
|     def parse_position_(self, enumerated, vertical):
 | |
|         assert self.cur_token_ in {"position", "pos"}
 | |
|         if self.next_token_ == "cursive":  # GPOS type 3
 | |
|             return self.parse_position_cursive_(enumerated, vertical)
 | |
|         elif self.next_token_ == "base":  # GPOS type 4
 | |
|             return self.parse_position_base_(enumerated, vertical)
 | |
|         elif self.next_token_ == "ligature":  # GPOS type 5
 | |
|             return self.parse_position_ligature_(enumerated, vertical)
 | |
|         elif self.next_token_ == "mark":  # GPOS type 6
 | |
|             return self.parse_position_mark_(enumerated, vertical)
 | |
| 
 | |
|         location = self.cur_token_location_
 | |
|         prefix, glyphs, lookups, values, suffix, hasMarks = self.parse_glyph_pattern_(
 | |
|             vertical
 | |
|         )
 | |
|         self.expect_symbol_(";")
 | |
| 
 | |
|         if any(lookups):
 | |
|             # GPOS type 8: Chaining contextual positioning; explicit lookups
 | |
|             if any(values):
 | |
|                 raise FeatureLibError(
 | |
|                     'If "lookup" is present, no values must be specified', location
 | |
|                 )
 | |
|             return self.ast.ChainContextPosStatement(
 | |
|                 prefix, glyphs, suffix, lookups, location=location
 | |
|             )
 | |
| 
 | |
|         # Pair positioning, format A: "pos V 10 A -10;"
 | |
|         # Pair positioning, format B: "pos V A -20;"
 | |
|         if not prefix and not suffix and len(glyphs) == 2 and not hasMarks:
 | |
|             if values[0] is None:  # Format B: "pos V A -20;"
 | |
|                 values.reverse()
 | |
|             return self.ast.PairPosStatement(
 | |
|                 glyphs[0],
 | |
|                 values[0],
 | |
|                 glyphs[1],
 | |
|                 values[1],
 | |
|                 enumerated=enumerated,
 | |
|                 location=location,
 | |
|             )
 | |
| 
 | |
|         if enumerated:
 | |
|             raise FeatureLibError(
 | |
|                 '"enumerate" is only allowed with pair positionings', location
 | |
|             )
 | |
|         return self.ast.SinglePosStatement(
 | |
|             list(zip(glyphs, values)),
 | |
|             prefix,
 | |
|             suffix,
 | |
|             forceChain=hasMarks,
 | |
|             location=location,
 | |
|         )
 | |
| 
 | |
|     def parse_position_cursive_(self, enumerated, vertical):
 | |
|         location = self.cur_token_location_
 | |
|         self.expect_keyword_("cursive")
 | |
|         if enumerated:
 | |
|             raise FeatureLibError(
 | |
|                 '"enumerate" is not allowed with ' "cursive attachment positioning",
 | |
|                 location,
 | |
|             )
 | |
|         glyphclass = self.parse_glyphclass_(accept_glyphname=True)
 | |
|         entryAnchor = self.parse_anchor_()
 | |
|         exitAnchor = self.parse_anchor_()
 | |
|         self.expect_symbol_(";")
 | |
|         return self.ast.CursivePosStatement(
 | |
|             glyphclass, entryAnchor, exitAnchor, location=location
 | |
|         )
 | |
| 
 | |
|     def parse_position_base_(self, enumerated, vertical):
 | |
|         location = self.cur_token_location_
 | |
|         self.expect_keyword_("base")
 | |
|         if enumerated:
 | |
|             raise FeatureLibError(
 | |
|                 '"enumerate" is not allowed with '
 | |
|                 "mark-to-base attachment positioning",
 | |
|                 location,
 | |
|             )
 | |
|         base = self.parse_glyphclass_(accept_glyphname=True)
 | |
|         marks = self.parse_anchor_marks_()
 | |
|         self.expect_symbol_(";")
 | |
|         return self.ast.MarkBasePosStatement(base, marks, location=location)
 | |
| 
 | |
|     def parse_position_ligature_(self, enumerated, vertical):
 | |
|         location = self.cur_token_location_
 | |
|         self.expect_keyword_("ligature")
 | |
|         if enumerated:
 | |
|             raise FeatureLibError(
 | |
|                 '"enumerate" is not allowed with '
 | |
|                 "mark-to-ligature attachment positioning",
 | |
|                 location,
 | |
|             )
 | |
|         ligatures = self.parse_glyphclass_(accept_glyphname=True)
 | |
|         marks = [self.parse_anchor_marks_()]
 | |
|         while self.next_token_ == "ligComponent":
 | |
|             self.expect_keyword_("ligComponent")
 | |
|             marks.append(self.parse_anchor_marks_())
 | |
|         self.expect_symbol_(";")
 | |
|         return self.ast.MarkLigPosStatement(ligatures, marks, location=location)
 | |
| 
 | |
|     def parse_position_mark_(self, enumerated, vertical):
 | |
|         location = self.cur_token_location_
 | |
|         self.expect_keyword_("mark")
 | |
|         if enumerated:
 | |
|             raise FeatureLibError(
 | |
|                 '"enumerate" is not allowed with '
 | |
|                 "mark-to-mark attachment positioning",
 | |
|                 location,
 | |
|             )
 | |
|         baseMarks = self.parse_glyphclass_(accept_glyphname=True)
 | |
|         marks = self.parse_anchor_marks_()
 | |
|         self.expect_symbol_(";")
 | |
|         return self.ast.MarkMarkPosStatement(baseMarks, marks, location=location)
 | |
| 
 | |
|     def parse_script_(self):
 | |
|         assert self.is_cur_keyword_("script")
 | |
|         location, script = self.cur_token_location_, self.expect_script_tag_()
 | |
|         self.expect_symbol_(";")
 | |
|         return self.ast.ScriptStatement(script, location=location)
 | |
| 
 | |
|     def parse_substitute_(self):
 | |
|         assert self.cur_token_ in {"substitute", "sub", "reversesub", "rsub"}
 | |
|         location = self.cur_token_location_
 | |
|         reverse = self.cur_token_ in {"reversesub", "rsub"}
 | |
|         (
 | |
|             old_prefix,
 | |
|             old,
 | |
|             lookups,
 | |
|             values,
 | |
|             old_suffix,
 | |
|             hasMarks,
 | |
|         ) = self.parse_glyph_pattern_(vertical=False)
 | |
|         if any(values):
 | |
|             raise FeatureLibError(
 | |
|                 "Substitution statements cannot contain values", location
 | |
|             )
 | |
|         new = []
 | |
|         if self.next_token_ == "by":
 | |
|             keyword = self.expect_keyword_("by")
 | |
|             while self.next_token_ != ";":
 | |
|                 gc = self.parse_glyphclass_(accept_glyphname=True, accept_null=True)
 | |
|                 new.append(gc)
 | |
|         elif self.next_token_ == "from":
 | |
|             keyword = self.expect_keyword_("from")
 | |
|             new = [self.parse_glyphclass_(accept_glyphname=False)]
 | |
|         else:
 | |
|             keyword = None
 | |
|         self.expect_symbol_(";")
 | |
|         if len(new) == 0 and not any(lookups):
 | |
|             raise FeatureLibError(
 | |
|                 'Expected "by", "from" or explicit lookup references',
 | |
|                 self.cur_token_location_,
 | |
|             )
 | |
| 
 | |
|         # GSUB lookup type 3: Alternate substitution.
 | |
|         # Format: "substitute a from [a.1 a.2 a.3];"
 | |
|         if keyword == "from":
 | |
|             if reverse:
 | |
|                 raise FeatureLibError(
 | |
|                     'Reverse chaining substitutions do not support "from"', location
 | |
|                 )
 | |
|             if len(old) != 1 or len(old[0].glyphSet()) != 1:
 | |
|                 raise FeatureLibError('Expected a single glyph before "from"', location)
 | |
|             if len(new) != 1:
 | |
|                 raise FeatureLibError(
 | |
|                     'Expected a single glyphclass after "from"', location
 | |
|                 )
 | |
|             return self.ast.AlternateSubstStatement(
 | |
|                 old_prefix, old[0], old_suffix, new[0], location=location
 | |
|             )
 | |
| 
 | |
|         num_lookups = len([l for l in lookups if l is not None])
 | |
| 
 | |
|         is_deletion = False
 | |
|         if len(new) == 1 and isinstance(new[0], ast.NullGlyph):
 | |
|             new = []  # Deletion
 | |
|             is_deletion = True
 | |
| 
 | |
|         # GSUB lookup type 1: Single substitution.
 | |
|         # Format A: "substitute a by a.sc;"
 | |
|         # Format B: "substitute [one.fitted one.oldstyle] by one;"
 | |
|         # Format C: "substitute [a-d] by [A.sc-D.sc];"
 | |
|         if not reverse and len(old) == 1 and len(new) == 1 and num_lookups == 0:
 | |
|             glyphs = list(old[0].glyphSet())
 | |
|             replacements = list(new[0].glyphSet())
 | |
|             if len(replacements) == 1:
 | |
|                 replacements = replacements * len(glyphs)
 | |
|             if len(glyphs) != len(replacements):
 | |
|                 raise FeatureLibError(
 | |
|                     'Expected a glyph class with %d elements after "by", '
 | |
|                     "but found a glyph class with %d elements"
 | |
|                     % (len(glyphs), len(replacements)),
 | |
|                     location,
 | |
|                 )
 | |
|             return self.ast.SingleSubstStatement(
 | |
|                 old, new, old_prefix, old_suffix, forceChain=hasMarks, location=location
 | |
|             )
 | |
| 
 | |
|         # Glyph deletion, built as GSUB lookup type 2: Multiple substitution
 | |
|         # with empty replacement.
 | |
|         if is_deletion and len(old) == 1 and num_lookups == 0:
 | |
|             return self.ast.MultipleSubstStatement(
 | |
|                 old_prefix,
 | |
|                 old[0],
 | |
|                 old_suffix,
 | |
|                 (),
 | |
|                 forceChain=hasMarks,
 | |
|                 location=location,
 | |
|             )
 | |
| 
 | |
|         # GSUB lookup type 2: Multiple substitution.
 | |
|         # Format: "substitute f_f_i by f f i;"
 | |
|         #
 | |
|         # GlyphsApp introduces two additional formats:
 | |
|         # Format 1: "substitute [f_i f_l] by [f f] [i l];"
 | |
|         # Format 2: "substitute [f_i f_l] by f [i l];"
 | |
|         # http://handbook.glyphsapp.com/en/layout/multiple-substitution-with-classes/
 | |
|         if not reverse and len(old) == 1 and len(new) > 1 and num_lookups == 0:
 | |
|             count = len(old[0].glyphSet())
 | |
|             for n in new:
 | |
|                 if not list(n.glyphSet()):
 | |
|                     raise FeatureLibError("Empty class in replacement", location)
 | |
|                 if len(n.glyphSet()) != 1 and len(n.glyphSet()) != count:
 | |
|                     raise FeatureLibError(
 | |
|                         f'Expected a glyph class with 1 or {count} elements after "by", '
 | |
|                         f"but found a glyph class with {len(n.glyphSet())} elements",
 | |
|                         location,
 | |
|                     )
 | |
|             return self.ast.MultipleSubstStatement(
 | |
|                 old_prefix,
 | |
|                 old[0],
 | |
|                 old_suffix,
 | |
|                 new,
 | |
|                 forceChain=hasMarks,
 | |
|                 location=location,
 | |
|             )
 | |
| 
 | |
|         # GSUB lookup type 4: Ligature substitution.
 | |
|         # Format: "substitute f f i by f_f_i;"
 | |
|         if (
 | |
|             not reverse
 | |
|             and len(old) > 1
 | |
|             and len(new) == 1
 | |
|             and len(new[0].glyphSet()) == 1
 | |
|             and num_lookups == 0
 | |
|         ):
 | |
|             return self.ast.LigatureSubstStatement(
 | |
|                 old_prefix,
 | |
|                 old,
 | |
|                 old_suffix,
 | |
|                 list(new[0].glyphSet())[0],
 | |
|                 forceChain=hasMarks,
 | |
|                 location=location,
 | |
|             )
 | |
| 
 | |
|         # GSUB lookup type 8: Reverse chaining substitution.
 | |
|         if reverse:
 | |
|             if len(old) != 1:
 | |
|                 raise FeatureLibError(
 | |
|                     "In reverse chaining single substitutions, "
 | |
|                     "only a single glyph or glyph class can be replaced",
 | |
|                     location,
 | |
|                 )
 | |
|             if len(new) != 1:
 | |
|                 raise FeatureLibError(
 | |
|                     "In reverse chaining single substitutions, "
 | |
|                     'the replacement (after "by") must be a single glyph '
 | |
|                     "or glyph class",
 | |
|                     location,
 | |
|                 )
 | |
|             if num_lookups != 0:
 | |
|                 raise FeatureLibError(
 | |
|                     "Reverse chaining substitutions cannot call named lookups", location
 | |
|                 )
 | |
|             glyphs = sorted(list(old[0].glyphSet()))
 | |
|             replacements = sorted(list(new[0].glyphSet()))
 | |
|             if len(replacements) == 1:
 | |
|                 replacements = replacements * len(glyphs)
 | |
|             if len(glyphs) != len(replacements):
 | |
|                 raise FeatureLibError(
 | |
|                     'Expected a glyph class with %d elements after "by", '
 | |
|                     "but found a glyph class with %d elements"
 | |
|                     % (len(glyphs), len(replacements)),
 | |
|                     location,
 | |
|                 )
 | |
|             return self.ast.ReverseChainSingleSubstStatement(
 | |
|                 old_prefix, old_suffix, old, new, location=location
 | |
|             )
 | |
| 
 | |
|         if len(old) > 1 and len(new) > 1:
 | |
|             raise FeatureLibError(
 | |
|                 "Direct substitution of multiple glyphs by multiple glyphs "
 | |
|                 "is not supported",
 | |
|                 location,
 | |
|             )
 | |
| 
 | |
|         # If there are remaining glyphs to parse, this is an invalid GSUB statement
 | |
|         if len(new) != 0 or is_deletion:
 | |
|             raise FeatureLibError("Invalid substitution statement", location)
 | |
| 
 | |
|         # GSUB lookup type 6: Chaining contextual substitution.
 | |
|         rule = self.ast.ChainContextSubstStatement(
 | |
|             old_prefix, old, old_suffix, lookups, location=location
 | |
|         )
 | |
|         return rule
 | |
| 
 | |
|     def parse_subtable_(self):
 | |
|         assert self.is_cur_keyword_("subtable")
 | |
|         location = self.cur_token_location_
 | |
|         self.expect_symbol_(";")
 | |
|         return self.ast.SubtableStatement(location=location)
 | |
| 
 | |
|     def parse_size_parameters_(self):
 | |
|         # Parses a ``parameters`` statement used in ``size`` features. See
 | |
|         # `section 8.b <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.b>`_.
 | |
|         assert self.is_cur_keyword_("parameters")
 | |
|         location = self.cur_token_location_
 | |
|         DesignSize = self.expect_decipoint_()
 | |
|         SubfamilyID = self.expect_number_()
 | |
|         RangeStart = 0.0
 | |
|         RangeEnd = 0.0
 | |
|         if self.next_token_type_ in (Lexer.NUMBER, Lexer.FLOAT) or SubfamilyID != 0:
 | |
|             RangeStart = self.expect_decipoint_()
 | |
|             RangeEnd = self.expect_decipoint_()
 | |
| 
 | |
|         self.expect_symbol_(";")
 | |
|         return self.ast.SizeParameters(
 | |
|             DesignSize, SubfamilyID, RangeStart, RangeEnd, location=location
 | |
|         )
 | |
| 
 | |
|     def parse_size_menuname_(self):
 | |
|         assert self.is_cur_keyword_("sizemenuname")
 | |
|         location = self.cur_token_location_
 | |
|         platformID, platEncID, langID, string = self.parse_name_()
 | |
|         return self.ast.FeatureNameStatement(
 | |
|             "size", platformID, platEncID, langID, string, location=location
 | |
|         )
 | |
| 
 | |
|     def parse_table_(self):
 | |
|         assert self.is_cur_keyword_("table")
 | |
|         location, name = self.cur_token_location_, self.expect_tag_()
 | |
|         table = self.ast.TableBlock(name, location=location)
 | |
|         self.expect_symbol_("{")
 | |
|         handler = {
 | |
|             "GDEF": self.parse_table_GDEF_,
 | |
|             "head": self.parse_table_head_,
 | |
|             "hhea": self.parse_table_hhea_,
 | |
|             "vhea": self.parse_table_vhea_,
 | |
|             "name": self.parse_table_name_,
 | |
|             "BASE": self.parse_table_BASE_,
 | |
|             "OS/2": self.parse_table_OS_2_,
 | |
|             "STAT": self.parse_table_STAT_,
 | |
|         }.get(name)
 | |
|         if handler:
 | |
|             handler(table)
 | |
|         else:
 | |
|             raise FeatureLibError(
 | |
|                 '"table %s" is not supported' % name.strip(), location
 | |
|             )
 | |
|         self.expect_symbol_("}")
 | |
|         end_tag = self.expect_tag_()
 | |
|         if end_tag != name:
 | |
|             raise FeatureLibError(
 | |
|                 'Expected "%s"' % name.strip(), self.cur_token_location_
 | |
|             )
 | |
|         self.expect_symbol_(";")
 | |
|         return table
 | |
| 
 | |
|     def parse_table_GDEF_(self, table):
 | |
|         statements = table.statements
 | |
|         while self.next_token_ != "}" or self.cur_comments_:
 | |
|             self.advance_lexer_(comments=True)
 | |
|             if self.cur_token_type_ is Lexer.COMMENT:
 | |
|                 statements.append(
 | |
|                     self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
 | |
|                 )
 | |
|             elif self.is_cur_keyword_("Attach"):
 | |
|                 statements.append(self.parse_attach_())
 | |
|             elif self.is_cur_keyword_("GlyphClassDef"):
 | |
|                 statements.append(self.parse_GlyphClassDef_())
 | |
|             elif self.is_cur_keyword_("LigatureCaretByIndex"):
 | |
|                 statements.append(self.parse_ligatureCaretByIndex_())
 | |
|             elif self.is_cur_keyword_("LigatureCaretByPos"):
 | |
|                 statements.append(self.parse_ligatureCaretByPos_())
 | |
|             elif self.cur_token_ == ";":
 | |
|                 continue
 | |
|             else:
 | |
|                 raise FeatureLibError(
 | |
|                     "Expected Attach, LigatureCaretByIndex, " "or LigatureCaretByPos",
 | |
|                     self.cur_token_location_,
 | |
|                 )
 | |
| 
 | |
|     def parse_table_head_(self, table):
 | |
|         statements = table.statements
 | |
|         while self.next_token_ != "}" or self.cur_comments_:
 | |
|             self.advance_lexer_(comments=True)
 | |
|             if self.cur_token_type_ is Lexer.COMMENT:
 | |
|                 statements.append(
 | |
|                     self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
 | |
|                 )
 | |
|             elif self.is_cur_keyword_("FontRevision"):
 | |
|                 statements.append(self.parse_FontRevision_())
 | |
|             elif self.cur_token_ == ";":
 | |
|                 continue
 | |
|             else:
 | |
|                 raise FeatureLibError("Expected FontRevision", self.cur_token_location_)
 | |
| 
 | |
|     def parse_table_hhea_(self, table):
 | |
|         statements = table.statements
 | |
|         fields = ("CaretOffset", "Ascender", "Descender", "LineGap")
 | |
|         while self.next_token_ != "}" or self.cur_comments_:
 | |
|             self.advance_lexer_(comments=True)
 | |
|             if self.cur_token_type_ is Lexer.COMMENT:
 | |
|                 statements.append(
 | |
|                     self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
 | |
|                 )
 | |
|             elif self.cur_token_type_ is Lexer.NAME and self.cur_token_ in fields:
 | |
|                 key = self.cur_token_.lower()
 | |
|                 value = self.expect_number_()
 | |
|                 statements.append(
 | |
|                     self.ast.HheaField(key, value, location=self.cur_token_location_)
 | |
|                 )
 | |
|                 if self.next_token_ != ";":
 | |
|                     raise FeatureLibError(
 | |
|                         "Incomplete statement", self.next_token_location_
 | |
|                     )
 | |
|             elif self.cur_token_ == ";":
 | |
|                 continue
 | |
|             else:
 | |
|                 raise FeatureLibError(
 | |
|                     "Expected CaretOffset, Ascender, " "Descender or LineGap",
 | |
|                     self.cur_token_location_,
 | |
|                 )
 | |
| 
 | |
|     def parse_table_vhea_(self, table):
 | |
|         statements = table.statements
 | |
|         fields = ("VertTypoAscender", "VertTypoDescender", "VertTypoLineGap")
 | |
|         while self.next_token_ != "}" or self.cur_comments_:
 | |
|             self.advance_lexer_(comments=True)
 | |
|             if self.cur_token_type_ is Lexer.COMMENT:
 | |
|                 statements.append(
 | |
|                     self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
 | |
|                 )
 | |
|             elif self.cur_token_type_ is Lexer.NAME and self.cur_token_ in fields:
 | |
|                 key = self.cur_token_.lower()
 | |
|                 value = self.expect_number_()
 | |
|                 statements.append(
 | |
|                     self.ast.VheaField(key, value, location=self.cur_token_location_)
 | |
|                 )
 | |
|                 if self.next_token_ != ";":
 | |
|                     raise FeatureLibError(
 | |
|                         "Incomplete statement", self.next_token_location_
 | |
|                     )
 | |
|             elif self.cur_token_ == ";":
 | |
|                 continue
 | |
|             else:
 | |
|                 raise FeatureLibError(
 | |
|                     "Expected VertTypoAscender, "
 | |
|                     "VertTypoDescender or VertTypoLineGap",
 | |
|                     self.cur_token_location_,
 | |
|                 )
 | |
| 
 | |
|     def parse_table_name_(self, table):
 | |
|         statements = table.statements
 | |
|         while self.next_token_ != "}" or self.cur_comments_:
 | |
|             self.advance_lexer_(comments=True)
 | |
|             if self.cur_token_type_ is Lexer.COMMENT:
 | |
|                 statements.append(
 | |
|                     self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
 | |
|                 )
 | |
|             elif self.is_cur_keyword_("nameid"):
 | |
|                 statement = self.parse_nameid_()
 | |
|                 if statement:
 | |
|                     statements.append(statement)
 | |
|             elif self.cur_token_ == ";":
 | |
|                 continue
 | |
|             else:
 | |
|                 raise FeatureLibError("Expected nameid", self.cur_token_location_)
 | |
| 
 | |
|     def parse_name_(self):
 | |
|         """Parses a name record. See `section 9.e <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.e>`_."""
 | |
|         platEncID = None
 | |
|         langID = None
 | |
|         if self.next_token_type_ in Lexer.NUMBERS:
 | |
|             platformID = self.expect_any_number_()
 | |
|             location = self.cur_token_location_
 | |
|             if platformID not in (1, 3):
 | |
|                 raise FeatureLibError("Expected platform id 1 or 3", location)
 | |
|             if self.next_token_type_ in Lexer.NUMBERS:
 | |
|                 platEncID = self.expect_any_number_()
 | |
|                 langID = self.expect_any_number_()
 | |
|         else:
 | |
|             platformID = 3
 | |
|             location = self.cur_token_location_
 | |
| 
 | |
|         if platformID == 1:  # Macintosh
 | |
|             platEncID = platEncID or 0  # Roman
 | |
|             langID = langID or 0  # English
 | |
|         else:  # 3, Windows
 | |
|             platEncID = platEncID or 1  # Unicode
 | |
|             langID = langID or 0x0409  # English
 | |
| 
 | |
|         string = self.expect_string_()
 | |
|         self.expect_symbol_(";")
 | |
| 
 | |
|         encoding = getEncoding(platformID, platEncID, langID)
 | |
|         if encoding is None:
 | |
|             raise FeatureLibError("Unsupported encoding", location)
 | |
|         unescaped = self.unescape_string_(string, encoding)
 | |
|         return platformID, platEncID, langID, unescaped
 | |
| 
 | |
|     def parse_stat_name_(self):
 | |
|         platEncID = None
 | |
|         langID = None
 | |
|         if self.next_token_type_ in Lexer.NUMBERS:
 | |
|             platformID = self.expect_any_number_()
 | |
|             location = self.cur_token_location_
 | |
|             if platformID not in (1, 3):
 | |
|                 raise FeatureLibError("Expected platform id 1 or 3", location)
 | |
|             if self.next_token_type_ in Lexer.NUMBERS:
 | |
|                 platEncID = self.expect_any_number_()
 | |
|                 langID = self.expect_any_number_()
 | |
|         else:
 | |
|             platformID = 3
 | |
|             location = self.cur_token_location_
 | |
| 
 | |
|         if platformID == 1:  # Macintosh
 | |
|             platEncID = platEncID or 0  # Roman
 | |
|             langID = langID or 0  # English
 | |
|         else:  # 3, Windows
 | |
|             platEncID = platEncID or 1  # Unicode
 | |
|             langID = langID or 0x0409  # English
 | |
| 
 | |
|         string = self.expect_string_()
 | |
|         encoding = getEncoding(platformID, platEncID, langID)
 | |
|         if encoding is None:
 | |
|             raise FeatureLibError("Unsupported encoding", location)
 | |
|         unescaped = self.unescape_string_(string, encoding)
 | |
|         return platformID, platEncID, langID, unescaped
 | |
| 
 | |
|     def parse_nameid_(self):
 | |
|         assert self.cur_token_ == "nameid", self.cur_token_
 | |
|         location, nameID = self.cur_token_location_, self.expect_any_number_()
 | |
|         if nameID > 32767:
 | |
|             raise FeatureLibError(
 | |
|                 "Name id value cannot be greater than 32767", self.cur_token_location_
 | |
|             )
 | |
|         platformID, platEncID, langID, string = self.parse_name_()
 | |
|         return self.ast.NameRecord(
 | |
|             nameID, platformID, platEncID, langID, string, location=location
 | |
|         )
 | |
| 
 | |
|     def unescape_string_(self, string, encoding):
 | |
|         if encoding == "utf_16_be":
 | |
|             s = re.sub(r"\\[0-9a-fA-F]{4}", self.unescape_unichr_, string)
 | |
|         else:
 | |
|             unescape = lambda m: self.unescape_byte_(m, encoding)
 | |
|             s = re.sub(r"\\[0-9a-fA-F]{2}", unescape, string)
 | |
|         # We now have a Unicode string, but it might contain surrogate pairs.
 | |
|         # We convert surrogates to actual Unicode by round-tripping through
 | |
|         # Python's UTF-16 codec in a special mode.
 | |
|         utf16 = tobytes(s, "utf_16_be", "surrogatepass")
 | |
|         return tostr(utf16, "utf_16_be")
 | |
| 
 | |
|     @staticmethod
 | |
|     def unescape_unichr_(match):
 | |
|         n = match.group(0)[1:]
 | |
|         return chr(int(n, 16))
 | |
| 
 | |
|     @staticmethod
 | |
|     def unescape_byte_(match, encoding):
 | |
|         n = match.group(0)[1:]
 | |
|         return bytechr(int(n, 16)).decode(encoding)
 | |
| 
 | |
|     def find_previous(self, statements, class_):
 | |
|         for previous in reversed(statements):
 | |
|             if isinstance(previous, self.ast.Comment):
 | |
|                 continue
 | |
|             elif isinstance(previous, class_):
 | |
|                 return previous
 | |
|             else:
 | |
|                 # If we find something that doesn't match what we're looking
 | |
|                 # for, and isn't a comment, fail
 | |
|                 return None
 | |
|         # Out of statements to look at
 | |
|         return None
 | |
| 
 | |
|     def parse_table_BASE_(self, table):
 | |
|         statements = table.statements
 | |
|         while self.next_token_ != "}" or self.cur_comments_:
 | |
|             self.advance_lexer_(comments=True)
 | |
|             if self.cur_token_type_ is Lexer.COMMENT:
 | |
|                 statements.append(
 | |
|                     self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
 | |
|                 )
 | |
|             elif self.is_cur_keyword_("HorizAxis.BaseTagList"):
 | |
|                 horiz_bases = self.parse_base_tag_list_()
 | |
|             elif self.is_cur_keyword_("HorizAxis.BaseScriptList"):
 | |
|                 horiz_scripts = self.parse_base_script_list_(len(horiz_bases))
 | |
|                 statements.append(
 | |
|                     self.ast.BaseAxis(
 | |
|                         horiz_bases,
 | |
|                         horiz_scripts,
 | |
|                         False,
 | |
|                         location=self.cur_token_location_,
 | |
|                     )
 | |
|                 )
 | |
|             elif self.is_cur_keyword_("HorizAxis.MinMax"):
 | |
|                 base_script_list = self.find_previous(statements, ast.BaseAxis)
 | |
|                 if base_script_list is None:
 | |
|                     raise FeatureLibError(
 | |
|                         "MinMax must be preceded by BaseScriptList",
 | |
|                         self.cur_token_location_,
 | |
|                     )
 | |
|                 if base_script_list.vertical:
 | |
|                     raise FeatureLibError(
 | |
|                         "HorizAxis.MinMax must be preceded by HorizAxis statements",
 | |
|                         self.cur_token_location_,
 | |
|                     )
 | |
|                 base_script_list.minmax.append(self.parse_base_minmax_())
 | |
|             elif self.is_cur_keyword_("VertAxis.BaseTagList"):
 | |
|                 vert_bases = self.parse_base_tag_list_()
 | |
|             elif self.is_cur_keyword_("VertAxis.BaseScriptList"):
 | |
|                 vert_scripts = self.parse_base_script_list_(len(vert_bases))
 | |
|                 statements.append(
 | |
|                     self.ast.BaseAxis(
 | |
|                         vert_bases,
 | |
|                         vert_scripts,
 | |
|                         True,
 | |
|                         location=self.cur_token_location_,
 | |
|                     )
 | |
|                 )
 | |
|             elif self.is_cur_keyword_("VertAxis.MinMax"):
 | |
|                 base_script_list = self.find_previous(statements, ast.BaseAxis)
 | |
|                 if base_script_list is None:
 | |
|                     raise FeatureLibError(
 | |
|                         "MinMax must be preceded by BaseScriptList",
 | |
|                         self.cur_token_location_,
 | |
|                     )
 | |
|                 if not base_script_list.vertical:
 | |
|                     raise FeatureLibError(
 | |
|                         "VertAxis.MinMax must be preceded by VertAxis statements",
 | |
|                         self.cur_token_location_,
 | |
|                     )
 | |
|                 base_script_list.minmax.append(self.parse_base_minmax_())
 | |
|             elif self.cur_token_ == ";":
 | |
|                 continue
 | |
| 
 | |
|     def parse_table_OS_2_(self, table):
 | |
|         statements = table.statements
 | |
|         numbers = (
 | |
|             "FSType",
 | |
|             "TypoAscender",
 | |
|             "TypoDescender",
 | |
|             "TypoLineGap",
 | |
|             "winAscent",
 | |
|             "winDescent",
 | |
|             "XHeight",
 | |
|             "CapHeight",
 | |
|             "WeightClass",
 | |
|             "WidthClass",
 | |
|             "LowerOpSize",
 | |
|             "UpperOpSize",
 | |
|         )
 | |
|         ranges = ("UnicodeRange", "CodePageRange")
 | |
|         while self.next_token_ != "}" or self.cur_comments_:
 | |
|             self.advance_lexer_(comments=True)
 | |
|             if self.cur_token_type_ is Lexer.COMMENT:
 | |
|                 statements.append(
 | |
|                     self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
 | |
|                 )
 | |
|             elif self.cur_token_type_ is Lexer.NAME:
 | |
|                 key = self.cur_token_.lower()
 | |
|                 value = None
 | |
|                 if self.cur_token_ in numbers:
 | |
|                     value = self.expect_number_()
 | |
|                 elif self.is_cur_keyword_("Panose"):
 | |
|                     value = []
 | |
|                     for i in range(10):
 | |
|                         value.append(self.expect_number_())
 | |
|                 elif self.cur_token_ in ranges:
 | |
|                     value = []
 | |
|                     while self.next_token_ != ";":
 | |
|                         value.append(self.expect_number_())
 | |
|                 elif self.is_cur_keyword_("Vendor"):
 | |
|                     value = self.expect_string_()
 | |
|                 statements.append(
 | |
|                     self.ast.OS2Field(key, value, location=self.cur_token_location_)
 | |
|                 )
 | |
|             elif self.cur_token_ == ";":
 | |
|                 continue
 | |
| 
 | |
|     def parse_STAT_ElidedFallbackName(self):
 | |
|         assert self.is_cur_keyword_("ElidedFallbackName")
 | |
|         self.expect_symbol_("{")
 | |
|         names = []
 | |
|         while self.next_token_ != "}" or self.cur_comments_:
 | |
|             self.advance_lexer_()
 | |
|             if self.is_cur_keyword_("name"):
 | |
|                 platformID, platEncID, langID, string = self.parse_stat_name_()
 | |
|                 nameRecord = self.ast.STATNameStatement(
 | |
|                     "stat",
 | |
|                     platformID,
 | |
|                     platEncID,
 | |
|                     langID,
 | |
|                     string,
 | |
|                     location=self.cur_token_location_,
 | |
|                 )
 | |
|                 names.append(nameRecord)
 | |
|             else:
 | |
|                 if self.cur_token_ != ";":
 | |
|                     raise FeatureLibError(
 | |
|                         f"Unexpected token {self.cur_token_} " f"in ElidedFallbackName",
 | |
|                         self.cur_token_location_,
 | |
|                     )
 | |
|         self.expect_symbol_("}")
 | |
|         if not names:
 | |
|             raise FeatureLibError('Expected "name"', self.cur_token_location_)
 | |
|         return names
 | |
| 
 | |
|     def parse_STAT_design_axis(self):
 | |
|         assert self.is_cur_keyword_("DesignAxis")
 | |
|         names = []
 | |
|         axisTag = self.expect_tag_()
 | |
|         if (
 | |
|             axisTag not in ("ital", "opsz", "slnt", "wdth", "wght")
 | |
|             and not axisTag.isupper()
 | |
|         ):
 | |
|             log.warning(f"Unregistered axis tag {axisTag} should be uppercase.")
 | |
|         axisOrder = self.expect_number_()
 | |
|         self.expect_symbol_("{")
 | |
|         while self.next_token_ != "}" or self.cur_comments_:
 | |
|             self.advance_lexer_()
 | |
|             if self.cur_token_type_ is Lexer.COMMENT:
 | |
|                 continue
 | |
|             elif self.is_cur_keyword_("name"):
 | |
|                 location = self.cur_token_location_
 | |
|                 platformID, platEncID, langID, string = self.parse_stat_name_()
 | |
|                 name = self.ast.STATNameStatement(
 | |
|                     "stat", platformID, platEncID, langID, string, location=location
 | |
|                 )
 | |
|                 names.append(name)
 | |
|             elif self.cur_token_ == ";":
 | |
|                 continue
 | |
|             else:
 | |
|                 raise FeatureLibError(
 | |
|                     f'Expected "name", got {self.cur_token_}', self.cur_token_location_
 | |
|                 )
 | |
| 
 | |
|         self.expect_symbol_("}")
 | |
|         return self.ast.STATDesignAxisStatement(
 | |
|             axisTag, axisOrder, names, self.cur_token_location_
 | |
|         )
 | |
| 
 | |
|     def parse_STAT_axis_value_(self):
 | |
|         assert self.is_cur_keyword_("AxisValue")
 | |
|         self.expect_symbol_("{")
 | |
|         locations = []
 | |
|         names = []
 | |
|         flags = 0
 | |
|         while self.next_token_ != "}" or self.cur_comments_:
 | |
|             self.advance_lexer_(comments=True)
 | |
|             if self.cur_token_type_ is Lexer.COMMENT:
 | |
|                 continue
 | |
|             elif self.is_cur_keyword_("name"):
 | |
|                 location = self.cur_token_location_
 | |
|                 platformID, platEncID, langID, string = self.parse_stat_name_()
 | |
|                 name = self.ast.STATNameStatement(
 | |
|                     "stat", platformID, platEncID, langID, string, location=location
 | |
|                 )
 | |
|                 names.append(name)
 | |
|             elif self.is_cur_keyword_("location"):
 | |
|                 location = self.parse_STAT_location()
 | |
|                 locations.append(location)
 | |
|             elif self.is_cur_keyword_("flag"):
 | |
|                 flags = self.expect_stat_flags()
 | |
|             elif self.cur_token_ == ";":
 | |
|                 continue
 | |
|             else:
 | |
|                 raise FeatureLibError(
 | |
|                     f"Unexpected token {self.cur_token_} " f"in AxisValue",
 | |
|                     self.cur_token_location_,
 | |
|                 )
 | |
|         self.expect_symbol_("}")
 | |
|         if not names:
 | |
|             raise FeatureLibError('Expected "Axis Name"', self.cur_token_location_)
 | |
|         if not locations:
 | |
|             raise FeatureLibError('Expected "Axis location"', self.cur_token_location_)
 | |
|         if len(locations) > 1:
 | |
|             for location in locations:
 | |
|                 if len(location.values) > 1:
 | |
|                     raise FeatureLibError(
 | |
|                         "Only one value is allowed in a "
 | |
|                         "Format 4 Axis Value Record, but "
 | |
|                         f"{len(location.values)} were found.",
 | |
|                         self.cur_token_location_,
 | |
|                     )
 | |
|             format4_tags = []
 | |
|             for location in locations:
 | |
|                 tag = location.tag
 | |
|                 if tag in format4_tags:
 | |
|                     raise FeatureLibError(
 | |
|                         f"Axis tag {tag} already " "defined.", self.cur_token_location_
 | |
|                     )
 | |
|                 format4_tags.append(tag)
 | |
| 
 | |
|         return self.ast.STATAxisValueStatement(
 | |
|             names, locations, flags, self.cur_token_location_
 | |
|         )
 | |
| 
 | |
|     def parse_STAT_location(self):
 | |
|         values = []
 | |
|         tag = self.expect_tag_()
 | |
|         if len(tag.strip()) != 4:
 | |
|             raise FeatureLibError(
 | |
|                 f"Axis tag {self.cur_token_} must be 4 " "characters",
 | |
|                 self.cur_token_location_,
 | |
|             )
 | |
| 
 | |
|         while self.next_token_ != ";":
 | |
|             if self.next_token_type_ is Lexer.FLOAT:
 | |
|                 value = self.expect_float_()
 | |
|                 values.append(value)
 | |
|             elif self.next_token_type_ is Lexer.NUMBER:
 | |
|                 value = self.expect_number_()
 | |
|                 values.append(value)
 | |
|             else:
 | |
|                 raise FeatureLibError(
 | |
|                     f'Unexpected value "{self.next_token_}". '
 | |
|                     "Expected integer or float.",
 | |
|                     self.next_token_location_,
 | |
|                 )
 | |
|         if len(values) == 3:
 | |
|             nominal, min_val, max_val = values
 | |
|             if nominal < min_val or nominal > max_val:
 | |
|                 raise FeatureLibError(
 | |
|                     f"Default value {nominal} is outside "
 | |
|                     f"of specified range "
 | |
|                     f"{min_val}-{max_val}.",
 | |
|                     self.next_token_location_,
 | |
|                 )
 | |
|         return self.ast.AxisValueLocationStatement(tag, values)
 | |
| 
 | |
|     def parse_table_STAT_(self, table):
 | |
|         statements = table.statements
 | |
|         design_axes = []
 | |
|         while self.next_token_ != "}" or self.cur_comments_:
 | |
|             self.advance_lexer_(comments=True)
 | |
|             if self.cur_token_type_ is Lexer.COMMENT:
 | |
|                 statements.append(
 | |
|                     self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
 | |
|                 )
 | |
|             elif self.cur_token_type_ is Lexer.NAME:
 | |
|                 if self.is_cur_keyword_("ElidedFallbackName"):
 | |
|                     names = self.parse_STAT_ElidedFallbackName()
 | |
|                     statements.append(self.ast.ElidedFallbackName(names))
 | |
|                 elif self.is_cur_keyword_("ElidedFallbackNameID"):
 | |
|                     value = self.expect_number_()
 | |
|                     statements.append(self.ast.ElidedFallbackNameID(value))
 | |
|                     self.expect_symbol_(";")
 | |
|                 elif self.is_cur_keyword_("DesignAxis"):
 | |
|                     designAxis = self.parse_STAT_design_axis()
 | |
|                     design_axes.append(designAxis.tag)
 | |
|                     statements.append(designAxis)
 | |
|                     self.expect_symbol_(";")
 | |
|                 elif self.is_cur_keyword_("AxisValue"):
 | |
|                     axisValueRecord = self.parse_STAT_axis_value_()
 | |
|                     for location in axisValueRecord.locations:
 | |
|                         if location.tag not in design_axes:
 | |
|                             # Tag must be defined in a DesignAxis before it
 | |
|                             # can be referenced
 | |
|                             raise FeatureLibError(
 | |
|                                 "DesignAxis not defined for " f"{location.tag}.",
 | |
|                                 self.cur_token_location_,
 | |
|                             )
 | |
|                     statements.append(axisValueRecord)
 | |
|                     self.expect_symbol_(";")
 | |
|                 else:
 | |
|                     raise FeatureLibError(
 | |
|                         f"Unexpected token {self.cur_token_}", self.cur_token_location_
 | |
|                     )
 | |
|             elif self.cur_token_ == ";":
 | |
|                 continue
 | |
| 
 | |
|     def parse_base_tag_list_(self):
 | |
|         # Parses BASE table entries. (See `section 9.a <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.a>`_)
 | |
|         assert self.cur_token_ in (
 | |
|             "HorizAxis.BaseTagList",
 | |
|             "VertAxis.BaseTagList",
 | |
|         ), self.cur_token_
 | |
|         bases = []
 | |
|         while self.next_token_ != ";":
 | |
|             bases.append(self.expect_script_tag_())
 | |
|         self.expect_symbol_(";")
 | |
|         return bases
 | |
| 
 | |
|     def parse_base_script_list_(self, count):
 | |
|         assert self.cur_token_ in (
 | |
|             "HorizAxis.BaseScriptList",
 | |
|             "VertAxis.BaseScriptList",
 | |
|         ), self.cur_token_
 | |
|         scripts = [self.parse_base_script_record_(count)]
 | |
|         while self.next_token_ == ",":
 | |
|             self.expect_symbol_(",")
 | |
|             scripts.append(self.parse_base_script_record_(count))
 | |
|         self.expect_symbol_(";")
 | |
|         return scripts
 | |
| 
 | |
|     def parse_base_script_record_(self, count):
 | |
|         script_tag = self.expect_script_tag_()
 | |
|         base_tag = self.expect_script_tag_()
 | |
|         coords = [self.expect_number_() for i in range(count)]
 | |
|         return script_tag, base_tag, coords
 | |
| 
 | |
|     def parse_base_minmax_(self):
 | |
|         script_tag = self.expect_script_tag_()
 | |
|         language = self.expect_language_tag_()
 | |
|         min_coord = self.expect_number_()
 | |
|         self.advance_lexer_()
 | |
|         if not (self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == ","):
 | |
|             raise FeatureLibError(
 | |
|                 "Expected a comma between min and max coordinates",
 | |
|                 self.cur_token_location_,
 | |
|             )
 | |
|         max_coord = self.expect_number_()
 | |
|         if self.next_token_ == ",":  # feature tag...
 | |
|             raise FeatureLibError(
 | |
|                 "Feature tags are not yet supported in BASE table",
 | |
|                 self.cur_token_location_,
 | |
|             )
 | |
| 
 | |
|         return script_tag, language, min_coord, max_coord
 | |
| 
 | |
|     def parse_device_(self):
 | |
|         result = None
 | |
|         self.expect_symbol_("<")
 | |
|         self.expect_keyword_("device")
 | |
|         if self.next_token_ == "NULL":
 | |
|             self.expect_keyword_("NULL")
 | |
|         else:
 | |
|             result = [(self.expect_number_(), self.expect_number_())]
 | |
|             while self.next_token_ == ",":
 | |
|                 self.expect_symbol_(",")
 | |
|                 result.append((self.expect_number_(), self.expect_number_()))
 | |
|             result = tuple(result)  # make it hashable
 | |
|         self.expect_symbol_(">")
 | |
|         return result
 | |
| 
 | |
|     def is_next_value_(self):
 | |
|         return (
 | |
|             self.next_token_type_ is Lexer.NUMBER
 | |
|             or self.next_token_ == "<"
 | |
|             or self.next_token_ == "("
 | |
|         )
 | |
| 
 | |
|     def parse_valuerecord_(self, vertical):
 | |
|         if (
 | |
|             self.next_token_type_ is Lexer.SYMBOL and self.next_token_ == "("
 | |
|         ) or self.next_token_type_ is Lexer.NUMBER:
 | |
|             number, location = (
 | |
|                 self.expect_number_(variable=True),
 | |
|                 self.cur_token_location_,
 | |
|             )
 | |
|             if vertical:
 | |
|                 val = self.ast.ValueRecord(
 | |
|                     yAdvance=number, vertical=vertical, location=location
 | |
|                 )
 | |
|             else:
 | |
|                 val = self.ast.ValueRecord(
 | |
|                     xAdvance=number, vertical=vertical, location=location
 | |
|                 )
 | |
|             return val
 | |
|         self.expect_symbol_("<")
 | |
|         location = self.cur_token_location_
 | |
|         if self.next_token_type_ is Lexer.NAME:
 | |
|             name = self.expect_name_()
 | |
|             if name == "NULL":
 | |
|                 self.expect_symbol_(">")
 | |
|                 return self.ast.ValueRecord()
 | |
|             vrd = self.valuerecords_.resolve(name)
 | |
|             if vrd is None:
 | |
|                 raise FeatureLibError(
 | |
|                     'Unknown valueRecordDef "%s"' % name, self.cur_token_location_
 | |
|                 )
 | |
|             value = vrd.value
 | |
|             xPlacement, yPlacement = (value.xPlacement, value.yPlacement)
 | |
|             xAdvance, yAdvance = (value.xAdvance, value.yAdvance)
 | |
|         else:
 | |
|             xPlacement, yPlacement, xAdvance, yAdvance = (
 | |
|                 self.expect_number_(variable=True),
 | |
|                 self.expect_number_(variable=True),
 | |
|                 self.expect_number_(variable=True),
 | |
|                 self.expect_number_(variable=True),
 | |
|             )
 | |
| 
 | |
|         if self.next_token_ == "<":
 | |
|             xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice = (
 | |
|                 self.parse_device_(),
 | |
|                 self.parse_device_(),
 | |
|                 self.parse_device_(),
 | |
|                 self.parse_device_(),
 | |
|             )
 | |
|             allDeltas = sorted(
 | |
|                 [
 | |
|                     delta
 | |
|                     for size, delta in (xPlaDevice if xPlaDevice else ())
 | |
|                     + (yPlaDevice if yPlaDevice else ())
 | |
|                     + (xAdvDevice if xAdvDevice else ())
 | |
|                     + (yAdvDevice if yAdvDevice else ())
 | |
|                 ]
 | |
|             )
 | |
|             if allDeltas[0] < -128 or allDeltas[-1] > 127:
 | |
|                 raise FeatureLibError(
 | |
|                     "Device value out of valid range (-128..127)",
 | |
|                     self.cur_token_location_,
 | |
|                 )
 | |
|         else:
 | |
|             xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice = (None, None, None, None)
 | |
| 
 | |
|         self.expect_symbol_(">")
 | |
|         return self.ast.ValueRecord(
 | |
|             xPlacement,
 | |
|             yPlacement,
 | |
|             xAdvance,
 | |
|             yAdvance,
 | |
|             xPlaDevice,
 | |
|             yPlaDevice,
 | |
|             xAdvDevice,
 | |
|             yAdvDevice,
 | |
|             vertical=vertical,
 | |
|             location=location,
 | |
|         )
 | |
| 
 | |
|     def parse_valuerecord_definition_(self, vertical):
 | |
|         # Parses a named value record definition. (See section `2.e.v <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#2.e.v>`_)
 | |
|         assert self.is_cur_keyword_("valueRecordDef")
 | |
|         location = self.cur_token_location_
 | |
|         value = self.parse_valuerecord_(vertical)
 | |
|         name = self.expect_name_()
 | |
|         self.expect_symbol_(";")
 | |
|         vrd = self.ast.ValueRecordDefinition(name, value, location=location)
 | |
|         self.valuerecords_.define(name, vrd)
 | |
|         return vrd
 | |
| 
 | |
|     def parse_languagesystem_(self):
 | |
|         assert self.cur_token_ == "languagesystem"
 | |
|         location = self.cur_token_location_
 | |
|         script = self.expect_script_tag_()
 | |
|         language = self.expect_language_tag_()
 | |
|         self.expect_symbol_(";")
 | |
|         return self.ast.LanguageSystemStatement(script, language, location=location)
 | |
| 
 | |
|     def parse_feature_block_(self, variation=False):
 | |
|         if variation:
 | |
|             assert self.cur_token_ == "variation"
 | |
|         else:
 | |
|             assert self.cur_token_ == "feature"
 | |
|         location = self.cur_token_location_
 | |
|         tag = self.expect_tag_()
 | |
|         vertical = tag in {"vkrn", "vpal", "vhal", "valt"}
 | |
| 
 | |
|         stylisticset = None
 | |
|         cv_feature = None
 | |
|         size_feature = False
 | |
|         if tag in self.SS_FEATURE_TAGS:
 | |
|             stylisticset = tag
 | |
|         elif tag in self.CV_FEATURE_TAGS:
 | |
|             cv_feature = tag
 | |
|         elif tag == "size":
 | |
|             size_feature = True
 | |
| 
 | |
|         if variation:
 | |
|             conditionset = self.expect_name_()
 | |
| 
 | |
|         use_extension = False
 | |
|         if self.next_token_ == "useExtension":
 | |
|             self.expect_keyword_("useExtension")
 | |
|             use_extension = True
 | |
| 
 | |
|         if variation:
 | |
|             block = self.ast.VariationBlock(
 | |
|                 tag, conditionset, use_extension=use_extension, location=location
 | |
|             )
 | |
|         else:
 | |
|             block = self.ast.FeatureBlock(
 | |
|                 tag, use_extension=use_extension, location=location
 | |
|             )
 | |
|         self.parse_block_(block, vertical, stylisticset, size_feature, cv_feature)
 | |
|         return block
 | |
| 
 | |
|     def parse_feature_reference_(self):
 | |
|         assert self.cur_token_ == "feature", self.cur_token_
 | |
|         location = self.cur_token_location_
 | |
|         featureName = self.expect_tag_()
 | |
|         self.expect_symbol_(";")
 | |
|         return self.ast.FeatureReferenceStatement(featureName, location=location)
 | |
| 
 | |
|     def parse_featureNames_(self, tag):
 | |
|         """Parses a ``featureNames`` statement found in stylistic set features.
 | |
|         See section `8.c <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.c>`_.
 | |
|         """
 | |
|         assert self.cur_token_ == "featureNames", self.cur_token_
 | |
|         block = self.ast.NestedBlock(
 | |
|             tag, self.cur_token_, location=self.cur_token_location_
 | |
|         )
 | |
|         self.expect_symbol_("{")
 | |
|         for symtab in self.symbol_tables_:
 | |
|             symtab.enter_scope()
 | |
|         while self.next_token_ != "}" or self.cur_comments_:
 | |
|             self.advance_lexer_(comments=True)
 | |
|             if self.cur_token_type_ is Lexer.COMMENT:
 | |
|                 block.statements.append(
 | |
|                     self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
 | |
|                 )
 | |
|             elif self.is_cur_keyword_("name"):
 | |
|                 location = self.cur_token_location_
 | |
|                 platformID, platEncID, langID, string = self.parse_name_()
 | |
|                 block.statements.append(
 | |
|                     self.ast.FeatureNameStatement(
 | |
|                         tag, platformID, platEncID, langID, string, location=location
 | |
|                     )
 | |
|                 )
 | |
|             elif self.cur_token_ == ";":
 | |
|                 continue
 | |
|             else:
 | |
|                 raise FeatureLibError('Expected "name"', self.cur_token_location_)
 | |
|         self.expect_symbol_("}")
 | |
|         for symtab in self.symbol_tables_:
 | |
|             symtab.exit_scope()
 | |
|         self.expect_symbol_(";")
 | |
|         return block
 | |
| 
 | |
|     def parse_cvParameters_(self, tag):
 | |
|         # Parses a ``cvParameters`` block found in Character Variant features.
 | |
|         # See section `8.d <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.d>`_.
 | |
|         assert self.cur_token_ == "cvParameters", self.cur_token_
 | |
|         block = self.ast.NestedBlock(
 | |
|             tag, self.cur_token_, location=self.cur_token_location_
 | |
|         )
 | |
|         self.expect_symbol_("{")
 | |
|         for symtab in self.symbol_tables_:
 | |
|             symtab.enter_scope()
 | |
| 
 | |
|         statements = block.statements
 | |
|         while self.next_token_ != "}" or self.cur_comments_:
 | |
|             self.advance_lexer_(comments=True)
 | |
|             if self.cur_token_type_ is Lexer.COMMENT:
 | |
|                 statements.append(
 | |
|                     self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
 | |
|                 )
 | |
|             elif self.is_cur_keyword_(
 | |
|                 {
 | |
|                     "FeatUILabelNameID",
 | |
|                     "FeatUITooltipTextNameID",
 | |
|                     "SampleTextNameID",
 | |
|                     "ParamUILabelNameID",
 | |
|                 }
 | |
|             ):
 | |
|                 statements.append(self.parse_cvNameIDs_(tag, self.cur_token_))
 | |
|             elif self.is_cur_keyword_("Character"):
 | |
|                 statements.append(self.parse_cvCharacter_(tag))
 | |
|             elif self.cur_token_ == ";":
 | |
|                 continue
 | |
|             else:
 | |
|                 raise FeatureLibError(
 | |
|                     "Expected statement: got {} {}".format(
 | |
|                         self.cur_token_type_, self.cur_token_
 | |
|                     ),
 | |
|                     self.cur_token_location_,
 | |
|                 )
 | |
| 
 | |
|         self.expect_symbol_("}")
 | |
|         for symtab in self.symbol_tables_:
 | |
|             symtab.exit_scope()
 | |
|         self.expect_symbol_(";")
 | |
|         return block
 | |
| 
 | |
|     def parse_cvNameIDs_(self, tag, block_name):
 | |
|         assert self.cur_token_ == block_name, self.cur_token_
 | |
|         block = self.ast.NestedBlock(tag, block_name, location=self.cur_token_location_)
 | |
|         self.expect_symbol_("{")
 | |
|         for symtab in self.symbol_tables_:
 | |
|             symtab.enter_scope()
 | |
|         while self.next_token_ != "}" or self.cur_comments_:
 | |
|             self.advance_lexer_(comments=True)
 | |
|             if self.cur_token_type_ is Lexer.COMMENT:
 | |
|                 block.statements.append(
 | |
|                     self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
 | |
|                 )
 | |
|             elif self.is_cur_keyword_("name"):
 | |
|                 location = self.cur_token_location_
 | |
|                 platformID, platEncID, langID, string = self.parse_name_()
 | |
|                 block.statements.append(
 | |
|                     self.ast.CVParametersNameStatement(
 | |
|                         tag,
 | |
|                         platformID,
 | |
|                         platEncID,
 | |
|                         langID,
 | |
|                         string,
 | |
|                         block_name,
 | |
|                         location=location,
 | |
|                     )
 | |
|                 )
 | |
|             elif self.cur_token_ == ";":
 | |
|                 continue
 | |
|             else:
 | |
|                 raise FeatureLibError('Expected "name"', self.cur_token_location_)
 | |
|         self.expect_symbol_("}")
 | |
|         for symtab in self.symbol_tables_:
 | |
|             symtab.exit_scope()
 | |
|         self.expect_symbol_(";")
 | |
|         return block
 | |
| 
 | |
|     def parse_cvCharacter_(self, tag):
 | |
|         assert self.cur_token_ == "Character", self.cur_token_
 | |
|         location, character = self.cur_token_location_, self.expect_any_number_()
 | |
|         self.expect_symbol_(";")
 | |
|         if not (0xFFFFFF >= character >= 0):
 | |
|             raise FeatureLibError(
 | |
|                 "Character value must be between "
 | |
|                 "{:#x} and {:#x}".format(0, 0xFFFFFF),
 | |
|                 location,
 | |
|             )
 | |
|         return self.ast.CharacterStatement(character, tag, location=location)
 | |
| 
 | |
|     def parse_FontRevision_(self):
 | |
|         # Parses a ``FontRevision`` statement found in the head table. See
 | |
|         # `section 9.c <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.c>`_.
 | |
|         assert self.cur_token_ == "FontRevision", self.cur_token_
 | |
|         location, version = self.cur_token_location_, self.expect_float_()
 | |
|         self.expect_symbol_(";")
 | |
|         if version <= 0:
 | |
|             raise FeatureLibError("Font revision numbers must be positive", location)
 | |
|         return self.ast.FontRevisionStatement(version, location=location)
 | |
| 
 | |
|     def parse_conditionset_(self):
 | |
|         name = self.expect_name_()
 | |
| 
 | |
|         conditions = {}
 | |
|         self.expect_symbol_("{")
 | |
| 
 | |
|         while self.next_token_ != "}":
 | |
|             self.advance_lexer_()
 | |
|             if self.cur_token_type_ is not Lexer.NAME:
 | |
|                 raise FeatureLibError("Expected an axis name", self.cur_token_location_)
 | |
| 
 | |
|             axis = self.cur_token_
 | |
|             if axis in conditions:
 | |
|                 raise FeatureLibError(
 | |
|                     f"Repeated condition for axis {axis}", self.cur_token_location_
 | |
|                 )
 | |
| 
 | |
|             if self.next_token_type_ is Lexer.FLOAT:
 | |
|                 min_value = self.expect_float_()
 | |
|             elif self.next_token_type_ is Lexer.NUMBER:
 | |
|                 min_value = self.expect_number_(variable=False)
 | |
| 
 | |
|             if self.next_token_type_ is Lexer.FLOAT:
 | |
|                 max_value = self.expect_float_()
 | |
|             elif self.next_token_type_ is Lexer.NUMBER:
 | |
|                 max_value = self.expect_number_(variable=False)
 | |
|             self.expect_symbol_(";")
 | |
| 
 | |
|             conditions[axis] = (min_value, max_value)
 | |
| 
 | |
|         self.expect_symbol_("}")
 | |
| 
 | |
|         finalname = self.expect_name_()
 | |
|         if finalname != name:
 | |
|             raise FeatureLibError('Expected "%s"' % name, self.cur_token_location_)
 | |
|         return self.ast.ConditionsetStatement(name, conditions)
 | |
| 
 | |
|     def parse_block_(
 | |
|         self, block, vertical, stylisticset=None, size_feature=False, cv_feature=None
 | |
|     ):
 | |
|         self.expect_symbol_("{")
 | |
|         for symtab in self.symbol_tables_:
 | |
|             symtab.enter_scope()
 | |
| 
 | |
|         statements = block.statements
 | |
|         while self.next_token_ != "}" or self.cur_comments_:
 | |
|             self.advance_lexer_(comments=True)
 | |
|             if self.cur_token_type_ is Lexer.COMMENT:
 | |
|                 statements.append(
 | |
|                     self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
 | |
|                 )
 | |
|             elif self.cur_token_type_ is Lexer.GLYPHCLASS:
 | |
|                 statements.append(self.parse_glyphclass_definition_())
 | |
|             elif self.is_cur_keyword_("anchorDef"):
 | |
|                 statements.append(self.parse_anchordef_())
 | |
|             elif self.is_cur_keyword_({"enum", "enumerate"}):
 | |
|                 statements.append(self.parse_enumerate_(vertical=vertical))
 | |
|             elif self.is_cur_keyword_("feature"):
 | |
|                 statements.append(self.parse_feature_reference_())
 | |
|             elif self.is_cur_keyword_("ignore"):
 | |
|                 statements.append(self.parse_ignore_())
 | |
|             elif self.is_cur_keyword_("language"):
 | |
|                 statements.append(self.parse_language_())
 | |
|             elif self.is_cur_keyword_("lookup"):
 | |
|                 statements.append(self.parse_lookup_(vertical))
 | |
|             elif self.is_cur_keyword_("lookupflag"):
 | |
|                 statements.append(self.parse_lookupflag_())
 | |
|             elif self.is_cur_keyword_("markClass"):
 | |
|                 statements.append(self.parse_markClass_())
 | |
|             elif self.is_cur_keyword_({"pos", "position"}):
 | |
|                 statements.append(
 | |
|                     self.parse_position_(enumerated=False, vertical=vertical)
 | |
|                 )
 | |
|             elif self.is_cur_keyword_("script"):
 | |
|                 statements.append(self.parse_script_())
 | |
|             elif self.is_cur_keyword_({"sub", "substitute", "rsub", "reversesub"}):
 | |
|                 statements.append(self.parse_substitute_())
 | |
|             elif self.is_cur_keyword_("subtable"):
 | |
|                 statements.append(self.parse_subtable_())
 | |
|             elif self.is_cur_keyword_("valueRecordDef"):
 | |
|                 statements.append(self.parse_valuerecord_definition_(vertical))
 | |
|             elif stylisticset and self.is_cur_keyword_("featureNames"):
 | |
|                 statements.append(self.parse_featureNames_(stylisticset))
 | |
|             elif cv_feature and self.is_cur_keyword_("cvParameters"):
 | |
|                 statements.append(self.parse_cvParameters_(cv_feature))
 | |
|             elif size_feature and self.is_cur_keyword_("parameters"):
 | |
|                 statements.append(self.parse_size_parameters_())
 | |
|             elif size_feature and self.is_cur_keyword_("sizemenuname"):
 | |
|                 statements.append(self.parse_size_menuname_())
 | |
|             elif (
 | |
|                 self.cur_token_type_ is Lexer.NAME
 | |
|                 and self.cur_token_ in self.extensions
 | |
|             ):
 | |
|                 statements.append(self.extensions[self.cur_token_](self))
 | |
|             elif self.cur_token_ == ";":
 | |
|                 continue
 | |
|             else:
 | |
|                 raise FeatureLibError(
 | |
|                     "Expected glyph class definition or statement: got {} {}".format(
 | |
|                         self.cur_token_type_, self.cur_token_
 | |
|                     ),
 | |
|                     self.cur_token_location_,
 | |
|                 )
 | |
| 
 | |
|         self.expect_symbol_("}")
 | |
|         for symtab in self.symbol_tables_:
 | |
|             symtab.exit_scope()
 | |
| 
 | |
|         name = self.expect_name_()
 | |
|         if name != block.name.strip():
 | |
|             raise FeatureLibError(
 | |
|                 'Expected "%s"' % block.name.strip(), self.cur_token_location_
 | |
|             )
 | |
|         self.expect_symbol_(";")
 | |
| 
 | |
|     def is_cur_keyword_(self, k):
 | |
|         if self.cur_token_type_ is Lexer.NAME:
 | |
|             if isinstance(k, type("")):  # basestring is gone in Python3
 | |
|                 return self.cur_token_ == k
 | |
|             else:
 | |
|                 return self.cur_token_ in k
 | |
|         return False
 | |
| 
 | |
|     def expect_class_name_(self):
 | |
|         self.advance_lexer_()
 | |
|         if self.cur_token_type_ is not Lexer.GLYPHCLASS:
 | |
|             raise FeatureLibError("Expected @NAME", self.cur_token_location_)
 | |
|         return self.cur_token_
 | |
| 
 | |
|     def expect_cid_(self):
 | |
|         self.advance_lexer_()
 | |
|         if self.cur_token_type_ is Lexer.CID:
 | |
|             return self.cur_token_
 | |
|         raise FeatureLibError("Expected a CID", self.cur_token_location_)
 | |
| 
 | |
|     def expect_filename_(self):
 | |
|         self.advance_lexer_()
 | |
|         if self.cur_token_type_ is not Lexer.FILENAME:
 | |
|             raise FeatureLibError("Expected file name", self.cur_token_location_)
 | |
|         return self.cur_token_
 | |
| 
 | |
|     def expect_glyph_(self):
 | |
|         self.advance_lexer_()
 | |
|         if self.cur_token_type_ is Lexer.NAME:
 | |
|             return self.cur_token_.lstrip("\\")
 | |
|         elif self.cur_token_type_ is Lexer.CID:
 | |
|             return "cid%05d" % self.cur_token_
 | |
|         raise FeatureLibError("Expected a glyph name or CID", self.cur_token_location_)
 | |
| 
 | |
|     def check_glyph_name_in_glyph_set(self, *names):
 | |
|         """Adds a glyph name (just `start`) or glyph names of a
 | |
|         range (`start` and `end`) which are not in the glyph set
 | |
|         to the "missing list" for future error reporting.
 | |
| 
 | |
|         If no glyph set is present, does nothing.
 | |
|         """
 | |
|         if self.glyphNames_:
 | |
|             for name in names:
 | |
|                 if name in self.glyphNames_:
 | |
|                     continue
 | |
|                 if name not in self.missing:
 | |
|                     self.missing[name] = self.cur_token_location_
 | |
| 
 | |
|     def expect_markClass_reference_(self):
 | |
|         name = self.expect_class_name_()
 | |
|         mc = self.glyphclasses_.resolve(name)
 | |
|         if mc is None:
 | |
|             raise FeatureLibError(
 | |
|                 "Unknown markClass @%s" % name, self.cur_token_location_
 | |
|             )
 | |
|         if not isinstance(mc, self.ast.MarkClass):
 | |
|             raise FeatureLibError(
 | |
|                 "@%s is not a markClass" % name, self.cur_token_location_
 | |
|             )
 | |
|         return mc
 | |
| 
 | |
|     def expect_tag_(self):
 | |
|         self.advance_lexer_()
 | |
|         if self.cur_token_type_ is not Lexer.NAME:
 | |
|             raise FeatureLibError("Expected a tag", self.cur_token_location_)
 | |
|         if len(self.cur_token_) > 4:
 | |
|             raise FeatureLibError(
 | |
|                 "Tags cannot be longer than 4 characters", self.cur_token_location_
 | |
|             )
 | |
|         return (self.cur_token_ + "    ")[:4]
 | |
| 
 | |
|     def expect_script_tag_(self):
 | |
|         tag = self.expect_tag_()
 | |
|         if tag == "dflt":
 | |
|             raise FeatureLibError(
 | |
|                 '"dflt" is not a valid script tag; use "DFLT" instead',
 | |
|                 self.cur_token_location_,
 | |
|             )
 | |
|         return tag
 | |
| 
 | |
|     def expect_language_tag_(self):
 | |
|         tag = self.expect_tag_()
 | |
|         if tag == "DFLT":
 | |
|             raise FeatureLibError(
 | |
|                 '"DFLT" is not a valid language tag; use "dflt" instead',
 | |
|                 self.cur_token_location_,
 | |
|             )
 | |
|         return tag
 | |
| 
 | |
|     def expect_symbol_(self, symbol):
 | |
|         self.advance_lexer_()
 | |
|         if self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == symbol:
 | |
|             return symbol
 | |
|         raise FeatureLibError("Expected '%s'" % symbol, self.cur_token_location_)
 | |
| 
 | |
|     def expect_keyword_(self, keyword):
 | |
|         self.advance_lexer_()
 | |
|         if self.cur_token_type_ is Lexer.NAME and self.cur_token_ == keyword:
 | |
|             return self.cur_token_
 | |
|         raise FeatureLibError('Expected "%s"' % keyword, self.cur_token_location_)
 | |
| 
 | |
|     def expect_name_(self):
 | |
|         self.advance_lexer_()
 | |
|         if self.cur_token_type_ is Lexer.NAME:
 | |
|             return self.cur_token_
 | |
|         raise FeatureLibError("Expected a name", self.cur_token_location_)
 | |
| 
 | |
|     def expect_number_(self, variable=False):
 | |
|         self.advance_lexer_()
 | |
|         if self.cur_token_type_ is Lexer.NUMBER:
 | |
|             return self.cur_token_
 | |
|         if variable and self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == "(":
 | |
|             return self.expect_variable_scalar_()
 | |
|         raise FeatureLibError("Expected a number", self.cur_token_location_)
 | |
| 
 | |
|     def expect_variable_scalar_(self):
 | |
|         self.advance_lexer_()  # "("
 | |
|         scalar = VariableScalar()
 | |
|         while True:
 | |
|             if self.cur_token_type_ == Lexer.SYMBOL and self.cur_token_ == ")":
 | |
|                 break
 | |
|             location, value = self.expect_master_()
 | |
|             scalar.add_value(location, value)
 | |
|         return scalar
 | |
| 
 | |
|     def expect_master_(self):
 | |
|         location = {}
 | |
|         while True:
 | |
|             if self.cur_token_type_ is not Lexer.NAME:
 | |
|                 raise FeatureLibError("Expected an axis name", self.cur_token_location_)
 | |
|             axis = self.cur_token_
 | |
|             self.advance_lexer_()
 | |
|             if not (self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == "="):
 | |
|                 raise FeatureLibError(
 | |
|                     "Expected an equals sign", self.cur_token_location_
 | |
|                 )
 | |
|             value = self.expect_number_()
 | |
|             location[axis] = value
 | |
|             if self.next_token_type_ is Lexer.NAME and self.next_token_[0] == ":":
 | |
|                 # Lexer has just read the value as a glyph name. We'll correct it later
 | |
|                 break
 | |
|             self.advance_lexer_()
 | |
|             if not (self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == ","):
 | |
|                 raise FeatureLibError(
 | |
|                     "Expected an comma or an equals sign", self.cur_token_location_
 | |
|                 )
 | |
|             self.advance_lexer_()
 | |
|         self.advance_lexer_()
 | |
|         value = int(self.cur_token_[1:])
 | |
|         self.advance_lexer_()
 | |
|         return location, value
 | |
| 
 | |
|     def expect_any_number_(self):
 | |
|         self.advance_lexer_()
 | |
|         if self.cur_token_type_ in Lexer.NUMBERS:
 | |
|             return self.cur_token_
 | |
|         raise FeatureLibError(
 | |
|             "Expected a decimal, hexadecimal or octal number", self.cur_token_location_
 | |
|         )
 | |
| 
 | |
|     def expect_float_(self):
 | |
|         self.advance_lexer_()
 | |
|         if self.cur_token_type_ is Lexer.FLOAT:
 | |
|             return self.cur_token_
 | |
|         raise FeatureLibError(
 | |
|             "Expected a floating-point number", self.cur_token_location_
 | |
|         )
 | |
| 
 | |
|     def expect_decipoint_(self):
 | |
|         if self.next_token_type_ == Lexer.FLOAT:
 | |
|             return self.expect_float_()
 | |
|         elif self.next_token_type_ is Lexer.NUMBER:
 | |
|             return self.expect_number_() / 10
 | |
|         else:
 | |
|             raise FeatureLibError(
 | |
|                 "Expected an integer or floating-point number", self.cur_token_location_
 | |
|             )
 | |
| 
 | |
|     def expect_stat_flags(self):
 | |
|         value = 0
 | |
|         flags = {
 | |
|             "OlderSiblingFontAttribute": 1,
 | |
|             "ElidableAxisValueName": 2,
 | |
|         }
 | |
|         while self.next_token_ != ";":
 | |
|             if self.next_token_ in flags:
 | |
|                 name = self.expect_name_()
 | |
|                 value = value | flags[name]
 | |
|             else:
 | |
|                 raise FeatureLibError(
 | |
|                     f"Unexpected STAT flag {self.cur_token_}", self.cur_token_location_
 | |
|                 )
 | |
|         return value
 | |
| 
 | |
|     def expect_stat_values_(self):
 | |
|         if self.next_token_type_ == Lexer.FLOAT:
 | |
|             return self.expect_float_()
 | |
|         elif self.next_token_type_ is Lexer.NUMBER:
 | |
|             return self.expect_number_()
 | |
|         else:
 | |
|             raise FeatureLibError(
 | |
|                 "Expected an integer or floating-point number", self.cur_token_location_
 | |
|             )
 | |
| 
 | |
|     def expect_string_(self):
 | |
|         self.advance_lexer_()
 | |
|         if self.cur_token_type_ is Lexer.STRING:
 | |
|             return self.cur_token_
 | |
|         raise FeatureLibError("Expected a string", self.cur_token_location_)
 | |
| 
 | |
|     def advance_lexer_(self, comments=False):
 | |
|         if comments and self.cur_comments_:
 | |
|             self.cur_token_type_ = Lexer.COMMENT
 | |
|             self.cur_token_, self.cur_token_location_ = self.cur_comments_.pop(0)
 | |
|             return
 | |
|         else:
 | |
|             self.cur_token_type_, self.cur_token_, self.cur_token_location_ = (
 | |
|                 self.next_token_type_,
 | |
|                 self.next_token_,
 | |
|                 self.next_token_location_,
 | |
|             )
 | |
|         while True:
 | |
|             try:
 | |
|                 (
 | |
|                     self.next_token_type_,
 | |
|                     self.next_token_,
 | |
|                     self.next_token_location_,
 | |
|                 ) = next(self.lexer_)
 | |
|             except StopIteration:
 | |
|                 self.next_token_type_, self.next_token_ = (None, None)
 | |
|             if self.next_token_type_ != Lexer.COMMENT:
 | |
|                 break
 | |
|             self.cur_comments_.append((self.next_token_, self.next_token_location_))
 | |
| 
 | |
|     @staticmethod
 | |
|     def reverse_string_(s):
 | |
|         """'abc' --> 'cba'"""
 | |
|         return "".join(reversed(list(s)))
 | |
| 
 | |
|     def make_cid_range_(self, location, start, limit):
 | |
|         """(location, 999, 1001) --> ["cid00999", "cid01000", "cid01001"]"""
 | |
|         result = list()
 | |
|         if start > limit:
 | |
|             raise FeatureLibError(
 | |
|                 "Bad range: start should be less than limit", location
 | |
|             )
 | |
|         for cid in range(start, limit + 1):
 | |
|             result.append("cid%05d" % cid)
 | |
|         return result
 | |
| 
 | |
|     def make_glyph_range_(self, location, start, limit):
 | |
|         """(location, "a.sc", "d.sc") --> ["a.sc", "b.sc", "c.sc", "d.sc"]"""
 | |
|         result = list()
 | |
|         if len(start) != len(limit):
 | |
|             raise FeatureLibError(
 | |
|                 'Bad range: "%s" and "%s" should have the same length' % (start, limit),
 | |
|                 location,
 | |
|             )
 | |
| 
 | |
|         rev = self.reverse_string_
 | |
|         prefix = os.path.commonprefix([start, limit])
 | |
|         suffix = rev(os.path.commonprefix([rev(start), rev(limit)]))
 | |
|         if len(suffix) > 0:
 | |
|             start_range = start[len(prefix) : -len(suffix)]
 | |
|             limit_range = limit[len(prefix) : -len(suffix)]
 | |
|         else:
 | |
|             start_range = start[len(prefix) :]
 | |
|             limit_range = limit[len(prefix) :]
 | |
| 
 | |
|         if start_range >= limit_range:
 | |
|             raise FeatureLibError(
 | |
|                 "Start of range must be smaller than its end", location
 | |
|             )
 | |
| 
 | |
|         uppercase = re.compile(r"^[A-Z]$")
 | |
|         if uppercase.match(start_range) and uppercase.match(limit_range):
 | |
|             for c in range(ord(start_range), ord(limit_range) + 1):
 | |
|                 result.append("%s%c%s" % (prefix, c, suffix))
 | |
|             return result
 | |
| 
 | |
|         lowercase = re.compile(r"^[a-z]$")
 | |
|         if lowercase.match(start_range) and lowercase.match(limit_range):
 | |
|             for c in range(ord(start_range), ord(limit_range) + 1):
 | |
|                 result.append("%s%c%s" % (prefix, c, suffix))
 | |
|             return result
 | |
| 
 | |
|         digits = re.compile(r"^[0-9]{1,3}$")
 | |
|         if digits.match(start_range) and digits.match(limit_range):
 | |
|             for i in range(int(start_range, 10), int(limit_range, 10) + 1):
 | |
|                 number = ("000" + str(i))[-len(start_range) :]
 | |
|                 result.append("%s%s%s" % (prefix, number, suffix))
 | |
|             return result
 | |
| 
 | |
|         raise FeatureLibError('Bad range: "%s-%s"' % (start, limit), location)
 | |
| 
 | |
| 
 | |
| class SymbolTable(object):
 | |
|     def __init__(self):
 | |
|         self.scopes_ = [{}]
 | |
| 
 | |
|     def enter_scope(self):
 | |
|         self.scopes_.append({})
 | |
| 
 | |
|     def exit_scope(self):
 | |
|         self.scopes_.pop()
 | |
| 
 | |
|     def define(self, name, item):
 | |
|         self.scopes_[-1][name] = item
 | |
| 
 | |
|     def resolve(self, name):
 | |
|         for scope in reversed(self.scopes_):
 | |
|             item = scope.get(name)
 | |
|             if item:
 | |
|                 return item
 | |
|         return None
 |