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