1from fontTools.feaLib.error import FeatureLibError 2from fontTools.feaLib.location import FeatureLibLocation 3from fontTools.misc.encodingTools import getEncoding 4from fontTools.misc.textTools import byteord, tobytes 5from collections import OrderedDict 6import itertools 7 8SHIFT = " " * 4 9 10__all__ = [ 11 "Element", 12 "FeatureFile", 13 "Comment", 14 "GlyphName", 15 "GlyphClass", 16 "GlyphClassName", 17 "MarkClassName", 18 "AnonymousBlock", 19 "Block", 20 "FeatureBlock", 21 "NestedBlock", 22 "LookupBlock", 23 "GlyphClassDefinition", 24 "GlyphClassDefStatement", 25 "MarkClass", 26 "MarkClassDefinition", 27 "AlternateSubstStatement", 28 "Anchor", 29 "AnchorDefinition", 30 "AttachStatement", 31 "AxisValueLocationStatement", 32 "BaseAxis", 33 "CVParametersNameStatement", 34 "ChainContextPosStatement", 35 "ChainContextSubstStatement", 36 "CharacterStatement", 37 "ConditionsetStatement", 38 "CursivePosStatement", 39 "ElidedFallbackName", 40 "ElidedFallbackNameID", 41 "Expression", 42 "FeatureNameStatement", 43 "FeatureReferenceStatement", 44 "FontRevisionStatement", 45 "HheaField", 46 "IgnorePosStatement", 47 "IgnoreSubstStatement", 48 "IncludeStatement", 49 "LanguageStatement", 50 "LanguageSystemStatement", 51 "LigatureCaretByIndexStatement", 52 "LigatureCaretByPosStatement", 53 "LigatureSubstStatement", 54 "LookupFlagStatement", 55 "LookupReferenceStatement", 56 "MarkBasePosStatement", 57 "MarkLigPosStatement", 58 "MarkMarkPosStatement", 59 "MultipleSubstStatement", 60 "NameRecord", 61 "OS2Field", 62 "PairPosStatement", 63 "ReverseChainSingleSubstStatement", 64 "ScriptStatement", 65 "SinglePosStatement", 66 "SingleSubstStatement", 67 "SizeParameters", 68 "Statement", 69 "STATAxisValueStatement", 70 "STATDesignAxisStatement", 71 "STATNameStatement", 72 "SubtableStatement", 73 "TableBlock", 74 "ValueRecord", 75 "ValueRecordDefinition", 76 "VheaField", 77] 78 79 80def deviceToString(device): 81 if device is None: 82 return "<device NULL>" 83 else: 84 return "<device %s>" % ", ".join("%d %d" % t for t in device) 85 86 87fea_keywords = set( 88 [ 89 "anchor", 90 "anchordef", 91 "anon", 92 "anonymous", 93 "by", 94 "contour", 95 "cursive", 96 "device", 97 "enum", 98 "enumerate", 99 "excludedflt", 100 "exclude_dflt", 101 "feature", 102 "from", 103 "ignore", 104 "ignorebaseglyphs", 105 "ignoreligatures", 106 "ignoremarks", 107 "include", 108 "includedflt", 109 "include_dflt", 110 "language", 111 "languagesystem", 112 "lookup", 113 "lookupflag", 114 "mark", 115 "markattachmenttype", 116 "markclass", 117 "nameid", 118 "null", 119 "parameters", 120 "pos", 121 "position", 122 "required", 123 "righttoleft", 124 "reversesub", 125 "rsub", 126 "script", 127 "sub", 128 "substitute", 129 "subtable", 130 "table", 131 "usemarkfilteringset", 132 "useextension", 133 "valuerecorddef", 134 "base", 135 "gdef", 136 "head", 137 "hhea", 138 "name", 139 "vhea", 140 "vmtx", 141 ] 142) 143 144 145def asFea(g): 146 if hasattr(g, "asFea"): 147 return g.asFea() 148 elif isinstance(g, tuple) and len(g) == 2: 149 return asFea(g[0]) + " - " + asFea(g[1]) # a range 150 elif g.lower() in fea_keywords: 151 return "\\" + g 152 else: 153 return g 154 155 156class Element(object): 157 """A base class representing "something" in a feature file.""" 158 159 def __init__(self, location=None): 160 #: location of this element as a `FeatureLibLocation` object. 161 if location and not isinstance(location, FeatureLibLocation): 162 location = FeatureLibLocation(*location) 163 self.location = location 164 165 def build(self, builder): 166 pass 167 168 def asFea(self, indent=""): 169 """Returns this element as a string of feature code. For block-type 170 elements (such as :class:`FeatureBlock`), the `indent` string is 171 added to the start of each line in the output.""" 172 raise NotImplementedError 173 174 def __str__(self): 175 return self.asFea() 176 177 178class Statement(Element): 179 pass 180 181 182class Expression(Element): 183 pass 184 185 186class Comment(Element): 187 """A comment in a feature file.""" 188 189 def __init__(self, text, location=None): 190 super(Comment, self).__init__(location) 191 #: Text of the comment 192 self.text = text 193 194 def asFea(self, indent=""): 195 return self.text 196 197 198class NullGlyph(Expression): 199 """The NULL glyph, used in glyph deletion substitutions.""" 200 201 def __init__(self, location=None): 202 Expression.__init__(self, location) 203 #: The name itself as a string 204 205 def glyphSet(self): 206 """The glyphs in this class as a tuple of :class:`GlyphName` objects.""" 207 return () 208 209 def asFea(self, indent=""): 210 return "NULL" 211 212 213class GlyphName(Expression): 214 """A single glyph name, such as ``cedilla``.""" 215 216 def __init__(self, glyph, location=None): 217 Expression.__init__(self, location) 218 #: The name itself as a string 219 self.glyph = glyph 220 221 def glyphSet(self): 222 """The glyphs in this class as a tuple of :class:`GlyphName` objects.""" 223 return (self.glyph,) 224 225 def asFea(self, indent=""): 226 return asFea(self.glyph) 227 228 229class GlyphClass(Expression): 230 """A glyph class, such as ``[acute cedilla grave]``.""" 231 232 def __init__(self, glyphs=None, location=None): 233 Expression.__init__(self, location) 234 #: The list of glyphs in this class, as :class:`GlyphName` objects. 235 self.glyphs = glyphs if glyphs is not None else [] 236 self.original = [] 237 self.curr = 0 238 239 def glyphSet(self): 240 """The glyphs in this class as a tuple of :class:`GlyphName` objects.""" 241 return tuple(self.glyphs) 242 243 def asFea(self, indent=""): 244 if len(self.original): 245 if self.curr < len(self.glyphs): 246 self.original.extend(self.glyphs[self.curr :]) 247 self.curr = len(self.glyphs) 248 return "[" + " ".join(map(asFea, self.original)) + "]" 249 else: 250 return "[" + " ".join(map(asFea, self.glyphs)) + "]" 251 252 def extend(self, glyphs): 253 """Add a list of :class:`GlyphName` objects to the class.""" 254 self.glyphs.extend(glyphs) 255 256 def append(self, glyph): 257 """Add a single :class:`GlyphName` object to the class.""" 258 self.glyphs.append(glyph) 259 260 def add_range(self, start, end, glyphs): 261 """Add a range (e.g. ``A-Z``) to the class. ``start`` and ``end`` 262 are either :class:`GlyphName` objects or strings representing the 263 start and end glyphs in the class, and ``glyphs`` is the full list of 264 :class:`GlyphName` objects in the range.""" 265 if self.curr < len(self.glyphs): 266 self.original.extend(self.glyphs[self.curr :]) 267 self.original.append((start, end)) 268 self.glyphs.extend(glyphs) 269 self.curr = len(self.glyphs) 270 271 def add_cid_range(self, start, end, glyphs): 272 """Add a range to the class by glyph ID. ``start`` and ``end`` are the 273 initial and final IDs, and ``glyphs`` is the full list of 274 :class:`GlyphName` objects in the range.""" 275 if self.curr < len(self.glyphs): 276 self.original.extend(self.glyphs[self.curr :]) 277 self.original.append(("\\{}".format(start), "\\{}".format(end))) 278 self.glyphs.extend(glyphs) 279 self.curr = len(self.glyphs) 280 281 def add_class(self, gc): 282 """Add glyphs from the given :class:`GlyphClassName` object to the 283 class.""" 284 if self.curr < len(self.glyphs): 285 self.original.extend(self.glyphs[self.curr :]) 286 self.original.append(gc) 287 self.glyphs.extend(gc.glyphSet()) 288 self.curr = len(self.glyphs) 289 290 291class GlyphClassName(Expression): 292 """A glyph class name, such as ``@FRENCH_MARKS``. This must be instantiated 293 with a :class:`GlyphClassDefinition` object.""" 294 295 def __init__(self, glyphclass, location=None): 296 Expression.__init__(self, location) 297 assert isinstance(glyphclass, GlyphClassDefinition) 298 self.glyphclass = glyphclass 299 300 def glyphSet(self): 301 """The glyphs in this class as a tuple of :class:`GlyphName` objects.""" 302 return tuple(self.glyphclass.glyphSet()) 303 304 def asFea(self, indent=""): 305 return "@" + self.glyphclass.name 306 307 308class MarkClassName(Expression): 309 """A mark class name, such as ``@FRENCH_MARKS`` defined with ``markClass``. 310 This must be instantiated with a :class:`MarkClass` object.""" 311 312 def __init__(self, markClass, location=None): 313 Expression.__init__(self, location) 314 assert isinstance(markClass, MarkClass) 315 self.markClass = markClass 316 317 def glyphSet(self): 318 """The glyphs in this class as a tuple of :class:`GlyphName` objects.""" 319 return self.markClass.glyphSet() 320 321 def asFea(self, indent=""): 322 return "@" + self.markClass.name 323 324 325class AnonymousBlock(Statement): 326 """An anonymous data block.""" 327 328 def __init__(self, tag, content, location=None): 329 Statement.__init__(self, location) 330 self.tag = tag #: string containing the block's "tag" 331 self.content = content #: block data as string 332 333 def asFea(self, indent=""): 334 res = "anon {} {{\n".format(self.tag) 335 res += self.content 336 res += "}} {};\n\n".format(self.tag) 337 return res 338 339 340class Block(Statement): 341 """A block of statements: feature, lookup, etc.""" 342 343 def __init__(self, location=None): 344 Statement.__init__(self, location) 345 self.statements = [] #: Statements contained in the block 346 347 def build(self, builder): 348 """When handed a 'builder' object of comparable interface to 349 :class:`fontTools.feaLib.builder`, walks the statements in this 350 block, calling the builder callbacks.""" 351 for s in self.statements: 352 s.build(builder) 353 354 def asFea(self, indent=""): 355 indent += SHIFT 356 return ( 357 indent 358 + ("\n" + indent).join([s.asFea(indent=indent) for s in self.statements]) 359 + "\n" 360 ) 361 362 363class FeatureFile(Block): 364 """The top-level element of the syntax tree, containing the whole feature 365 file in its ``statements`` attribute.""" 366 367 def __init__(self): 368 Block.__init__(self, location=None) 369 self.markClasses = {} # name --> ast.MarkClass 370 371 def asFea(self, indent=""): 372 return "\n".join(s.asFea(indent=indent) for s in self.statements) 373 374 375class FeatureBlock(Block): 376 """A named feature block.""" 377 378 def __init__(self, name, use_extension=False, location=None): 379 Block.__init__(self, location) 380 self.name, self.use_extension = name, use_extension 381 382 def build(self, builder): 383 """Call the ``start_feature`` callback on the builder object, visit 384 all the statements in this feature, and then call ``end_feature``.""" 385 # TODO(sascha): Handle use_extension. 386 builder.start_feature(self.location, self.name) 387 # language exclude_dflt statements modify builder.features_ 388 # limit them to this block with temporary builder.features_ 389 features = builder.features_ 390 builder.features_ = {} 391 Block.build(self, builder) 392 for key, value in builder.features_.items(): 393 features.setdefault(key, []).extend(value) 394 builder.features_ = features 395 builder.end_feature() 396 397 def asFea(self, indent=""): 398 res = indent + "feature %s " % self.name.strip() 399 if self.use_extension: 400 res += "useExtension " 401 res += "{\n" 402 res += Block.asFea(self, indent=indent) 403 res += indent + "} %s;\n" % self.name.strip() 404 return res 405 406 407class NestedBlock(Block): 408 """A block inside another block, for example when found inside a 409 ``cvParameters`` block.""" 410 411 def __init__(self, tag, block_name, location=None): 412 Block.__init__(self, location) 413 self.tag = tag 414 self.block_name = block_name 415 416 def build(self, builder): 417 Block.build(self, builder) 418 if self.block_name == "ParamUILabelNameID": 419 builder.add_to_cv_num_named_params(self.tag) 420 421 def asFea(self, indent=""): 422 res = "{}{} {{\n".format(indent, self.block_name) 423 res += Block.asFea(self, indent=indent) 424 res += "{}}};\n".format(indent) 425 return res 426 427 428class LookupBlock(Block): 429 """A named lookup, containing ``statements``.""" 430 431 def __init__(self, name, use_extension=False, location=None): 432 Block.__init__(self, location) 433 self.name, self.use_extension = name, use_extension 434 435 def build(self, builder): 436 # TODO(sascha): Handle use_extension. 437 builder.start_lookup_block(self.location, self.name) 438 Block.build(self, builder) 439 builder.end_lookup_block() 440 441 def asFea(self, indent=""): 442 res = "lookup {} ".format(self.name) 443 if self.use_extension: 444 res += "useExtension " 445 res += "{\n" 446 res += Block.asFea(self, indent=indent) 447 res += "{}}} {};\n".format(indent, self.name) 448 return res 449 450 451class TableBlock(Block): 452 """A ``table ... { }`` block.""" 453 454 def __init__(self, name, location=None): 455 Block.__init__(self, location) 456 self.name = name 457 458 def asFea(self, indent=""): 459 res = "table {} {{\n".format(self.name.strip()) 460 res += super(TableBlock, self).asFea(indent=indent) 461 res += "}} {};\n".format(self.name.strip()) 462 return res 463 464 465class GlyphClassDefinition(Statement): 466 """Example: ``@UPPERCASE = [A-Z];``.""" 467 468 def __init__(self, name, glyphs, location=None): 469 Statement.__init__(self, location) 470 self.name = name #: class name as a string, without initial ``@`` 471 self.glyphs = glyphs #: a :class:`GlyphClass` object 472 473 def glyphSet(self): 474 """The glyphs in this class as a tuple of :class:`GlyphName` objects.""" 475 return tuple(self.glyphs.glyphSet()) 476 477 def asFea(self, indent=""): 478 return "@" + self.name + " = " + self.glyphs.asFea() + ";" 479 480 481class GlyphClassDefStatement(Statement): 482 """Example: ``GlyphClassDef @UPPERCASE, [B], [C], [D];``. The parameters 483 must be either :class:`GlyphClass` or :class:`GlyphClassName` objects, or 484 ``None``.""" 485 486 def __init__( 487 self, baseGlyphs, markGlyphs, ligatureGlyphs, componentGlyphs, location=None 488 ): 489 Statement.__init__(self, location) 490 self.baseGlyphs, self.markGlyphs = (baseGlyphs, markGlyphs) 491 self.ligatureGlyphs = ligatureGlyphs 492 self.componentGlyphs = componentGlyphs 493 494 def build(self, builder): 495 """Calls the builder's ``add_glyphClassDef`` callback.""" 496 base = self.baseGlyphs.glyphSet() if self.baseGlyphs else tuple() 497 liga = self.ligatureGlyphs.glyphSet() if self.ligatureGlyphs else tuple() 498 mark = self.markGlyphs.glyphSet() if self.markGlyphs else tuple() 499 comp = self.componentGlyphs.glyphSet() if self.componentGlyphs else tuple() 500 builder.add_glyphClassDef(self.location, base, liga, mark, comp) 501 502 def asFea(self, indent=""): 503 return "GlyphClassDef {}, {}, {}, {};".format( 504 self.baseGlyphs.asFea() if self.baseGlyphs else "", 505 self.ligatureGlyphs.asFea() if self.ligatureGlyphs else "", 506 self.markGlyphs.asFea() if self.markGlyphs else "", 507 self.componentGlyphs.asFea() if self.componentGlyphs else "", 508 ) 509 510 511class MarkClass(object): 512 """One `or more` ``markClass`` statements for the same mark class. 513 514 While glyph classes can be defined only once, the feature file format 515 allows expanding mark classes with multiple definitions, each using 516 different glyphs and anchors. The following are two ``MarkClassDefinitions`` 517 for the same ``MarkClass``:: 518 519 markClass [acute grave] <anchor 350 800> @FRENCH_ACCENTS; 520 markClass [cedilla] <anchor 350 -200> @FRENCH_ACCENTS; 521 522 The ``MarkClass`` object is therefore just a container for a list of 523 :class:`MarkClassDefinition` statements. 524 """ 525 526 def __init__(self, name): 527 self.name = name 528 self.definitions = [] 529 self.glyphs = OrderedDict() # glyph --> ast.MarkClassDefinitions 530 531 def addDefinition(self, definition): 532 """Add a :class:`MarkClassDefinition` statement to this mark class.""" 533 assert isinstance(definition, MarkClassDefinition) 534 self.definitions.append(definition) 535 for glyph in definition.glyphSet(): 536 if glyph in self.glyphs: 537 otherLoc = self.glyphs[glyph].location 538 if otherLoc is None: 539 end = "" 540 else: 541 end = f" at {otherLoc}" 542 raise FeatureLibError( 543 "Glyph %s already defined%s" % (glyph, end), definition.location 544 ) 545 self.glyphs[glyph] = definition 546 547 def glyphSet(self): 548 """The glyphs in this class as a tuple of :class:`GlyphName` objects.""" 549 return tuple(self.glyphs.keys()) 550 551 def asFea(self, indent=""): 552 res = "\n".join(d.asFea() for d in self.definitions) 553 return res 554 555 556class MarkClassDefinition(Statement): 557 """A single ``markClass`` statement. The ``markClass`` should be a 558 :class:`MarkClass` object, the ``anchor`` an :class:`Anchor` object, 559 and the ``glyphs`` parameter should be a `glyph-containing object`_ . 560 561 Example: 562 563 .. code:: python 564 565 mc = MarkClass("FRENCH_ACCENTS") 566 mc.addDefinition( MarkClassDefinition(mc, Anchor(350, 800), 567 GlyphClass([ GlyphName("acute"), GlyphName("grave") ]) 568 ) ) 569 mc.addDefinition( MarkClassDefinition(mc, Anchor(350, -200), 570 GlyphClass([ GlyphName("cedilla") ]) 571 ) ) 572 573 mc.asFea() 574 # markClass [acute grave] <anchor 350 800> @FRENCH_ACCENTS; 575 # markClass [cedilla] <anchor 350 -200> @FRENCH_ACCENTS; 576 577 """ 578 579 def __init__(self, markClass, anchor, glyphs, location=None): 580 Statement.__init__(self, location) 581 assert isinstance(markClass, MarkClass) 582 assert isinstance(anchor, Anchor) and isinstance(glyphs, Expression) 583 self.markClass, self.anchor, self.glyphs = markClass, anchor, glyphs 584 585 def glyphSet(self): 586 """The glyphs in this class as a tuple of :class:`GlyphName` objects.""" 587 return self.glyphs.glyphSet() 588 589 def asFea(self, indent=""): 590 return "markClass {} {} @{};".format( 591 self.glyphs.asFea(), self.anchor.asFea(), self.markClass.name 592 ) 593 594 595class AlternateSubstStatement(Statement): 596 """A ``sub ... from ...`` statement. 597 598 ``prefix``, ``glyph``, ``suffix`` and ``replacement`` should be lists of 599 `glyph-containing objects`_. ``glyph`` should be a `one element list`.""" 600 601 def __init__(self, prefix, glyph, suffix, replacement, location=None): 602 Statement.__init__(self, location) 603 self.prefix, self.glyph, self.suffix = (prefix, glyph, suffix) 604 self.replacement = replacement 605 606 def build(self, builder): 607 """Calls the builder's ``add_alternate_subst`` callback.""" 608 glyph = self.glyph.glyphSet() 609 assert len(glyph) == 1, glyph 610 glyph = list(glyph)[0] 611 prefix = [p.glyphSet() for p in self.prefix] 612 suffix = [s.glyphSet() for s in self.suffix] 613 replacement = self.replacement.glyphSet() 614 builder.add_alternate_subst(self.location, prefix, glyph, suffix, replacement) 615 616 def asFea(self, indent=""): 617 res = "sub " 618 if len(self.prefix) or len(self.suffix): 619 if len(self.prefix): 620 res += " ".join(map(asFea, self.prefix)) + " " 621 res += asFea(self.glyph) + "'" # even though we really only use 1 622 if len(self.suffix): 623 res += " " + " ".join(map(asFea, self.suffix)) 624 else: 625 res += asFea(self.glyph) 626 res += " from " 627 res += asFea(self.replacement) 628 res += ";" 629 return res 630 631 632class Anchor(Expression): 633 """An ``Anchor`` element, used inside a ``pos`` rule. 634 635 If a ``name`` is given, this will be used in preference to the coordinates. 636 Other values should be integer. 637 """ 638 639 def __init__( 640 self, 641 x, 642 y, 643 name=None, 644 contourpoint=None, 645 xDeviceTable=None, 646 yDeviceTable=None, 647 location=None, 648 ): 649 Expression.__init__(self, location) 650 self.name = name 651 self.x, self.y, self.contourpoint = x, y, contourpoint 652 self.xDeviceTable, self.yDeviceTable = xDeviceTable, yDeviceTable 653 654 def asFea(self, indent=""): 655 if self.name is not None: 656 return "<anchor {}>".format(self.name) 657 res = "<anchor {} {}".format(self.x, self.y) 658 if self.contourpoint: 659 res += " contourpoint {}".format(self.contourpoint) 660 if self.xDeviceTable or self.yDeviceTable: 661 res += " " 662 res += deviceToString(self.xDeviceTable) 663 res += " " 664 res += deviceToString(self.yDeviceTable) 665 res += ">" 666 return res 667 668 669class AnchorDefinition(Statement): 670 """A named anchor definition. (2.e.viii). ``name`` should be a string.""" 671 672 def __init__(self, name, x, y, contourpoint=None, location=None): 673 Statement.__init__(self, location) 674 self.name, self.x, self.y, self.contourpoint = name, x, y, contourpoint 675 676 def asFea(self, indent=""): 677 res = "anchorDef {} {}".format(self.x, self.y) 678 if self.contourpoint: 679 res += " contourpoint {}".format(self.contourpoint) 680 res += " {};".format(self.name) 681 return res 682 683 684class AttachStatement(Statement): 685 """A ``GDEF`` table ``Attach`` statement.""" 686 687 def __init__(self, glyphs, contourPoints, location=None): 688 Statement.__init__(self, location) 689 self.glyphs = glyphs #: A `glyph-containing object`_ 690 self.contourPoints = contourPoints #: A list of integer contour points 691 692 def build(self, builder): 693 """Calls the builder's ``add_attach_points`` callback.""" 694 glyphs = self.glyphs.glyphSet() 695 builder.add_attach_points(self.location, glyphs, self.contourPoints) 696 697 def asFea(self, indent=""): 698 return "Attach {} {};".format( 699 self.glyphs.asFea(), " ".join(str(c) for c in self.contourPoints) 700 ) 701 702 703class ChainContextPosStatement(Statement): 704 r"""A chained contextual positioning statement. 705 706 ``prefix``, ``glyphs``, and ``suffix`` should be lists of 707 `glyph-containing objects`_ . 708 709 ``lookups`` should be a list of elements representing what lookups 710 to apply at each glyph position. Each element should be a 711 :class:`LookupBlock` to apply a single chaining lookup at the given 712 position, a list of :class:`LookupBlock`\ s to apply multiple 713 lookups, or ``None`` to apply no lookup. The length of the outer 714 list should equal the length of ``glyphs``; the inner lists can be 715 of variable length.""" 716 717 def __init__(self, prefix, glyphs, suffix, lookups, location=None): 718 Statement.__init__(self, location) 719 self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix 720 self.lookups = list(lookups) 721 for i, lookup in enumerate(lookups): 722 if lookup: 723 try: 724 (_ for _ in lookup) 725 except TypeError: 726 self.lookups[i] = [lookup] 727 728 def build(self, builder): 729 """Calls the builder's ``add_chain_context_pos`` callback.""" 730 prefix = [p.glyphSet() for p in self.prefix] 731 glyphs = [g.glyphSet() for g in self.glyphs] 732 suffix = [s.glyphSet() for s in self.suffix] 733 builder.add_chain_context_pos( 734 self.location, prefix, glyphs, suffix, self.lookups 735 ) 736 737 def asFea(self, indent=""): 738 res = "pos " 739 if ( 740 len(self.prefix) 741 or len(self.suffix) 742 or any([x is not None for x in self.lookups]) 743 ): 744 if len(self.prefix): 745 res += " ".join(g.asFea() for g in self.prefix) + " " 746 for i, g in enumerate(self.glyphs): 747 res += g.asFea() + "'" 748 if self.lookups[i]: 749 for lu in self.lookups[i]: 750 res += " lookup " + lu.name 751 if i < len(self.glyphs) - 1: 752 res += " " 753 if len(self.suffix): 754 res += " " + " ".join(map(asFea, self.suffix)) 755 else: 756 res += " ".join(map(asFea, self.glyph)) 757 res += ";" 758 return res 759 760 761class ChainContextSubstStatement(Statement): 762 r"""A chained contextual substitution statement. 763 764 ``prefix``, ``glyphs``, and ``suffix`` should be lists of 765 `glyph-containing objects`_ . 766 767 ``lookups`` should be a list of elements representing what lookups 768 to apply at each glyph position. Each element should be a 769 :class:`LookupBlock` to apply a single chaining lookup at the given 770 position, a list of :class:`LookupBlock`\ s to apply multiple 771 lookups, or ``None`` to apply no lookup. The length of the outer 772 list should equal the length of ``glyphs``; the inner lists can be 773 of variable length.""" 774 775 def __init__(self, prefix, glyphs, suffix, lookups, location=None): 776 Statement.__init__(self, location) 777 self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix 778 self.lookups = list(lookups) 779 for i, lookup in enumerate(lookups): 780 if lookup: 781 try: 782 (_ for _ in lookup) 783 except TypeError: 784 self.lookups[i] = [lookup] 785 786 def build(self, builder): 787 """Calls the builder's ``add_chain_context_subst`` callback.""" 788 prefix = [p.glyphSet() for p in self.prefix] 789 glyphs = [g.glyphSet() for g in self.glyphs] 790 suffix = [s.glyphSet() for s in self.suffix] 791 builder.add_chain_context_subst( 792 self.location, prefix, glyphs, suffix, self.lookups 793 ) 794 795 def asFea(self, indent=""): 796 res = "sub " 797 if ( 798 len(self.prefix) 799 or len(self.suffix) 800 or any([x is not None for x in self.lookups]) 801 ): 802 if len(self.prefix): 803 res += " ".join(g.asFea() for g in self.prefix) + " " 804 for i, g in enumerate(self.glyphs): 805 res += g.asFea() + "'" 806 if self.lookups[i]: 807 for lu in self.lookups[i]: 808 res += " lookup " + lu.name 809 if i < len(self.glyphs) - 1: 810 res += " " 811 if len(self.suffix): 812 res += " " + " ".join(map(asFea, self.suffix)) 813 else: 814 res += " ".join(map(asFea, self.glyph)) 815 res += ";" 816 return res 817 818 819class CursivePosStatement(Statement): 820 """A cursive positioning statement. Entry and exit anchors can either 821 be :class:`Anchor` objects or ``None``.""" 822 823 def __init__(self, glyphclass, entryAnchor, exitAnchor, location=None): 824 Statement.__init__(self, location) 825 self.glyphclass = glyphclass 826 self.entryAnchor, self.exitAnchor = entryAnchor, exitAnchor 827 828 def build(self, builder): 829 """Calls the builder object's ``add_cursive_pos`` callback.""" 830 builder.add_cursive_pos( 831 self.location, self.glyphclass.glyphSet(), self.entryAnchor, self.exitAnchor 832 ) 833 834 def asFea(self, indent=""): 835 entry = self.entryAnchor.asFea() if self.entryAnchor else "<anchor NULL>" 836 exit = self.exitAnchor.asFea() if self.exitAnchor else "<anchor NULL>" 837 return "pos cursive {} {} {};".format(self.glyphclass.asFea(), entry, exit) 838 839 840class FeatureReferenceStatement(Statement): 841 """Example: ``feature salt;``""" 842 843 def __init__(self, featureName, location=None): 844 Statement.__init__(self, location) 845 self.location, self.featureName = (location, featureName) 846 847 def build(self, builder): 848 """Calls the builder object's ``add_feature_reference`` callback.""" 849 builder.add_feature_reference(self.location, self.featureName) 850 851 def asFea(self, indent=""): 852 return "feature {};".format(self.featureName) 853 854 855class IgnorePosStatement(Statement): 856 """An ``ignore pos`` statement, containing `one or more` contexts to ignore. 857 858 ``chainContexts`` should be a list of ``(prefix, glyphs, suffix)`` tuples, 859 with each of ``prefix``, ``glyphs`` and ``suffix`` being 860 `glyph-containing objects`_ .""" 861 862 def __init__(self, chainContexts, location=None): 863 Statement.__init__(self, location) 864 self.chainContexts = chainContexts 865 866 def build(self, builder): 867 """Calls the builder object's ``add_chain_context_pos`` callback on each 868 rule context.""" 869 for prefix, glyphs, suffix in self.chainContexts: 870 prefix = [p.glyphSet() for p in prefix] 871 glyphs = [g.glyphSet() for g in glyphs] 872 suffix = [s.glyphSet() for s in suffix] 873 builder.add_chain_context_pos(self.location, prefix, glyphs, suffix, []) 874 875 def asFea(self, indent=""): 876 contexts = [] 877 for prefix, glyphs, suffix in self.chainContexts: 878 res = "" 879 if len(prefix) or len(suffix): 880 if len(prefix): 881 res += " ".join(map(asFea, prefix)) + " " 882 res += " ".join(g.asFea() + "'" for g in glyphs) 883 if len(suffix): 884 res += " " + " ".join(map(asFea, suffix)) 885 else: 886 res += " ".join(map(asFea, glyphs)) 887 contexts.append(res) 888 return "ignore pos " + ", ".join(contexts) + ";" 889 890 891class IgnoreSubstStatement(Statement): 892 """An ``ignore sub`` statement, containing `one or more` contexts to ignore. 893 894 ``chainContexts`` should be a list of ``(prefix, glyphs, suffix)`` tuples, 895 with each of ``prefix``, ``glyphs`` and ``suffix`` being 896 `glyph-containing objects`_ .""" 897 898 def __init__(self, chainContexts, location=None): 899 Statement.__init__(self, location) 900 self.chainContexts = chainContexts 901 902 def build(self, builder): 903 """Calls the builder object's ``add_chain_context_subst`` callback on 904 each rule context.""" 905 for prefix, glyphs, suffix in self.chainContexts: 906 prefix = [p.glyphSet() for p in prefix] 907 glyphs = [g.glyphSet() for g in glyphs] 908 suffix = [s.glyphSet() for s in suffix] 909 builder.add_chain_context_subst(self.location, prefix, glyphs, suffix, []) 910 911 def asFea(self, indent=""): 912 contexts = [] 913 for prefix, glyphs, suffix in self.chainContexts: 914 res = "" 915 if len(prefix) or len(suffix): 916 if len(prefix): 917 res += " ".join(map(asFea, prefix)) + " " 918 res += " ".join(g.asFea() + "'" for g in glyphs) 919 if len(suffix): 920 res += " " + " ".join(map(asFea, suffix)) 921 else: 922 res += " ".join(map(asFea, glyphs)) 923 contexts.append(res) 924 return "ignore sub " + ", ".join(contexts) + ";" 925 926 927class IncludeStatement(Statement): 928 """An ``include()`` statement.""" 929 930 def __init__(self, filename, location=None): 931 super(IncludeStatement, self).__init__(location) 932 self.filename = filename #: String containing name of file to include 933 934 def build(self): 935 # TODO: consider lazy-loading the including parser/lexer? 936 raise FeatureLibError( 937 "Building an include statement is not implemented yet. " 938 "Instead, use Parser(..., followIncludes=True) for building.", 939 self.location, 940 ) 941 942 def asFea(self, indent=""): 943 return indent + "include(%s);" % self.filename 944 945 946class LanguageStatement(Statement): 947 """A ``language`` statement within a feature.""" 948 949 def __init__(self, language, include_default=True, required=False, location=None): 950 Statement.__init__(self, location) 951 assert len(language) == 4 952 self.language = language #: A four-character language tag 953 self.include_default = include_default #: If false, "exclude_dflt" 954 self.required = required 955 956 def build(self, builder): 957 """Call the builder object's ``set_language`` callback.""" 958 builder.set_language( 959 location=self.location, 960 language=self.language, 961 include_default=self.include_default, 962 required=self.required, 963 ) 964 965 def asFea(self, indent=""): 966 res = "language {}".format(self.language.strip()) 967 if not self.include_default: 968 res += " exclude_dflt" 969 if self.required: 970 res += " required" 971 res += ";" 972 return res 973 974 975class LanguageSystemStatement(Statement): 976 """A top-level ``languagesystem`` statement.""" 977 978 def __init__(self, script, language, location=None): 979 Statement.__init__(self, location) 980 self.script, self.language = (script, language) 981 982 def build(self, builder): 983 """Calls the builder object's ``add_language_system`` callback.""" 984 builder.add_language_system(self.location, self.script, self.language) 985 986 def asFea(self, indent=""): 987 return "languagesystem {} {};".format(self.script, self.language.strip()) 988 989 990class FontRevisionStatement(Statement): 991 """A ``head`` table ``FontRevision`` statement. ``revision`` should be a 992 number, and will be formatted to three significant decimal places.""" 993 994 def __init__(self, revision, location=None): 995 Statement.__init__(self, location) 996 self.revision = revision 997 998 def build(self, builder): 999 builder.set_font_revision(self.location, self.revision) 1000 1001 def asFea(self, indent=""): 1002 return "FontRevision {:.3f};".format(self.revision) 1003 1004 1005class LigatureCaretByIndexStatement(Statement): 1006 """A ``GDEF`` table ``LigatureCaretByIndex`` statement. ``glyphs`` should be 1007 a `glyph-containing object`_, and ``carets`` should be a list of integers.""" 1008 1009 def __init__(self, glyphs, carets, location=None): 1010 Statement.__init__(self, location) 1011 self.glyphs, self.carets = (glyphs, carets) 1012 1013 def build(self, builder): 1014 """Calls the builder object's ``add_ligatureCaretByIndex_`` callback.""" 1015 glyphs = self.glyphs.glyphSet() 1016 builder.add_ligatureCaretByIndex_(self.location, glyphs, set(self.carets)) 1017 1018 def asFea(self, indent=""): 1019 return "LigatureCaretByIndex {} {};".format( 1020 self.glyphs.asFea(), " ".join(str(x) for x in self.carets) 1021 ) 1022 1023 1024class LigatureCaretByPosStatement(Statement): 1025 """A ``GDEF`` table ``LigatureCaretByPos`` statement. ``glyphs`` should be 1026 a `glyph-containing object`_, and ``carets`` should be a list of integers.""" 1027 1028 def __init__(self, glyphs, carets, location=None): 1029 Statement.__init__(self, location) 1030 self.glyphs, self.carets = (glyphs, carets) 1031 1032 def build(self, builder): 1033 """Calls the builder object's ``add_ligatureCaretByPos_`` callback.""" 1034 glyphs = self.glyphs.glyphSet() 1035 builder.add_ligatureCaretByPos_(self.location, glyphs, set(self.carets)) 1036 1037 def asFea(self, indent=""): 1038 return "LigatureCaretByPos {} {};".format( 1039 self.glyphs.asFea(), " ".join(str(x) for x in self.carets) 1040 ) 1041 1042 1043class LigatureSubstStatement(Statement): 1044 """A chained contextual substitution statement. 1045 1046 ``prefix``, ``glyphs``, and ``suffix`` should be lists of 1047 `glyph-containing objects`_; ``replacement`` should be a single 1048 `glyph-containing object`_. 1049 1050 If ``forceChain`` is True, this is expressed as a chaining rule 1051 (e.g. ``sub f' i' by f_i``) even when no context is given.""" 1052 1053 def __init__(self, prefix, glyphs, suffix, replacement, forceChain, location=None): 1054 Statement.__init__(self, location) 1055 self.prefix, self.glyphs, self.suffix = (prefix, glyphs, suffix) 1056 self.replacement, self.forceChain = replacement, forceChain 1057 1058 def build(self, builder): 1059 prefix = [p.glyphSet() for p in self.prefix] 1060 glyphs = [g.glyphSet() for g in self.glyphs] 1061 suffix = [s.glyphSet() for s in self.suffix] 1062 builder.add_ligature_subst( 1063 self.location, prefix, glyphs, suffix, self.replacement, self.forceChain 1064 ) 1065 1066 def asFea(self, indent=""): 1067 res = "sub " 1068 if len(self.prefix) or len(self.suffix) or self.forceChain: 1069 if len(self.prefix): 1070 res += " ".join(g.asFea() for g in self.prefix) + " " 1071 res += " ".join(g.asFea() + "'" for g in self.glyphs) 1072 if len(self.suffix): 1073 res += " " + " ".join(g.asFea() for g in self.suffix) 1074 else: 1075 res += " ".join(g.asFea() for g in self.glyphs) 1076 res += " by " 1077 res += asFea(self.replacement) 1078 res += ";" 1079 return res 1080 1081 1082class LookupFlagStatement(Statement): 1083 """A ``lookupflag`` statement. The ``value`` should be an integer value 1084 representing the flags in use, but not including the ``markAttachment`` 1085 class and ``markFilteringSet`` values, which must be specified as 1086 glyph-containing objects.""" 1087 1088 def __init__( 1089 self, value=0, markAttachment=None, markFilteringSet=None, location=None 1090 ): 1091 Statement.__init__(self, location) 1092 self.value = value 1093 self.markAttachment = markAttachment 1094 self.markFilteringSet = markFilteringSet 1095 1096 def build(self, builder): 1097 """Calls the builder object's ``set_lookup_flag`` callback.""" 1098 markAttach = None 1099 if self.markAttachment is not None: 1100 markAttach = self.markAttachment.glyphSet() 1101 markFilter = None 1102 if self.markFilteringSet is not None: 1103 markFilter = self.markFilteringSet.glyphSet() 1104 builder.set_lookup_flag(self.location, self.value, markAttach, markFilter) 1105 1106 def asFea(self, indent=""): 1107 res = [] 1108 flags = ["RightToLeft", "IgnoreBaseGlyphs", "IgnoreLigatures", "IgnoreMarks"] 1109 curr = 1 1110 for i in range(len(flags)): 1111 if self.value & curr != 0: 1112 res.append(flags[i]) 1113 curr = curr << 1 1114 if self.markAttachment is not None: 1115 res.append("MarkAttachmentType {}".format(self.markAttachment.asFea())) 1116 if self.markFilteringSet is not None: 1117 res.append("UseMarkFilteringSet {}".format(self.markFilteringSet.asFea())) 1118 if not res: 1119 res = ["0"] 1120 return "lookupflag {};".format(" ".join(res)) 1121 1122 1123class LookupReferenceStatement(Statement): 1124 """Represents a ``lookup ...;`` statement to include a lookup in a feature. 1125 1126 The ``lookup`` should be a :class:`LookupBlock` object.""" 1127 1128 def __init__(self, lookup, location=None): 1129 Statement.__init__(self, location) 1130 self.location, self.lookup = (location, lookup) 1131 1132 def build(self, builder): 1133 """Calls the builder object's ``add_lookup_call`` callback.""" 1134 builder.add_lookup_call(self.lookup.name) 1135 1136 def asFea(self, indent=""): 1137 return "lookup {};".format(self.lookup.name) 1138 1139 1140class MarkBasePosStatement(Statement): 1141 """A mark-to-base positioning rule. The ``base`` should be a 1142 `glyph-containing object`_. The ``marks`` should be a list of 1143 (:class:`Anchor`, :class:`MarkClass`) tuples.""" 1144 1145 def __init__(self, base, marks, location=None): 1146 Statement.__init__(self, location) 1147 self.base, self.marks = base, marks 1148 1149 def build(self, builder): 1150 """Calls the builder object's ``add_mark_base_pos`` callback.""" 1151 builder.add_mark_base_pos(self.location, self.base.glyphSet(), self.marks) 1152 1153 def asFea(self, indent=""): 1154 res = "pos base {}".format(self.base.asFea()) 1155 for a, m in self.marks: 1156 res += "\n" + indent + SHIFT + "{} mark @{}".format(a.asFea(), m.name) 1157 res += ";" 1158 return res 1159 1160 1161class MarkLigPosStatement(Statement): 1162 """A mark-to-ligature positioning rule. The ``ligatures`` must be a 1163 `glyph-containing object`_. The ``marks`` should be a list of lists: each 1164 element in the top-level list represents a component glyph, and is made 1165 up of a list of (:class:`Anchor`, :class:`MarkClass`) tuples representing 1166 mark attachment points for that position. 1167 1168 Example:: 1169 1170 m1 = MarkClass("TOP_MARKS") 1171 m2 = MarkClass("BOTTOM_MARKS") 1172 # ... add definitions to mark classes... 1173 1174 glyph = GlyphName("lam_meem_jeem") 1175 marks = [ 1176 [ (Anchor(625,1800), m1) ], # Attachments on 1st component (lam) 1177 [ (Anchor(376,-378), m2) ], # Attachments on 2nd component (meem) 1178 [ ] # No attachments on the jeem 1179 ] 1180 mlp = MarkLigPosStatement(glyph, marks) 1181 1182 mlp.asFea() 1183 # pos ligature lam_meem_jeem <anchor 625 1800> mark @TOP_MARKS 1184 # ligComponent <anchor 376 -378> mark @BOTTOM_MARKS; 1185 1186 """ 1187 1188 def __init__(self, ligatures, marks, location=None): 1189 Statement.__init__(self, location) 1190 self.ligatures, self.marks = ligatures, marks 1191 1192 def build(self, builder): 1193 """Calls the builder object's ``add_mark_lig_pos`` callback.""" 1194 builder.add_mark_lig_pos(self.location, self.ligatures.glyphSet(), self.marks) 1195 1196 def asFea(self, indent=""): 1197 res = "pos ligature {}".format(self.ligatures.asFea()) 1198 ligs = [] 1199 for l in self.marks: 1200 temp = "" 1201 if l is None or not len(l): 1202 temp = "\n" + indent + SHIFT * 2 + "<anchor NULL>" 1203 else: 1204 for a, m in l: 1205 temp += ( 1206 "\n" 1207 + indent 1208 + SHIFT * 2 1209 + "{} mark @{}".format(a.asFea(), m.name) 1210 ) 1211 ligs.append(temp) 1212 res += ("\n" + indent + SHIFT + "ligComponent").join(ligs) 1213 res += ";" 1214 return res 1215 1216 1217class MarkMarkPosStatement(Statement): 1218 """A mark-to-mark positioning rule. The ``baseMarks`` must be a 1219 `glyph-containing object`_. The ``marks`` should be a list of 1220 (:class:`Anchor`, :class:`MarkClass`) tuples.""" 1221 1222 def __init__(self, baseMarks, marks, location=None): 1223 Statement.__init__(self, location) 1224 self.baseMarks, self.marks = baseMarks, marks 1225 1226 def build(self, builder): 1227 """Calls the builder object's ``add_mark_mark_pos`` callback.""" 1228 builder.add_mark_mark_pos(self.location, self.baseMarks.glyphSet(), self.marks) 1229 1230 def asFea(self, indent=""): 1231 res = "pos mark {}".format(self.baseMarks.asFea()) 1232 for a, m in self.marks: 1233 res += "\n" + indent + SHIFT + "{} mark @{}".format(a.asFea(), m.name) 1234 res += ";" 1235 return res 1236 1237 1238class MultipleSubstStatement(Statement): 1239 """A multiple substitution statement. 1240 1241 Args: 1242 prefix: a list of `glyph-containing objects`_. 1243 glyph: a single glyph-containing object. 1244 suffix: a list of glyph-containing objects. 1245 replacement: a list of glyph-containing objects. 1246 forceChain: If true, the statement is expressed as a chaining rule 1247 (e.g. ``sub f' i' by f_i``) even when no context is given. 1248 """ 1249 1250 def __init__( 1251 self, prefix, glyph, suffix, replacement, forceChain=False, location=None 1252 ): 1253 Statement.__init__(self, location) 1254 self.prefix, self.glyph, self.suffix = prefix, glyph, suffix 1255 self.replacement = replacement 1256 self.forceChain = forceChain 1257 1258 def build(self, builder): 1259 """Calls the builder object's ``add_multiple_subst`` callback.""" 1260 prefix = [p.glyphSet() for p in self.prefix] 1261 suffix = [s.glyphSet() for s in self.suffix] 1262 if not self.replacement and hasattr(self.glyph, "glyphSet"): 1263 for glyph in self.glyph.glyphSet(): 1264 builder.add_multiple_subst( 1265 self.location, 1266 prefix, 1267 glyph, 1268 suffix, 1269 self.replacement, 1270 self.forceChain, 1271 ) 1272 else: 1273 builder.add_multiple_subst( 1274 self.location, 1275 prefix, 1276 self.glyph, 1277 suffix, 1278 self.replacement, 1279 self.forceChain, 1280 ) 1281 1282 def asFea(self, indent=""): 1283 res = "sub " 1284 if len(self.prefix) or len(self.suffix) or self.forceChain: 1285 if len(self.prefix): 1286 res += " ".join(map(asFea, self.prefix)) + " " 1287 res += asFea(self.glyph) + "'" 1288 if len(self.suffix): 1289 res += " " + " ".join(map(asFea, self.suffix)) 1290 else: 1291 res += asFea(self.glyph) 1292 replacement = self.replacement or [NullGlyph()] 1293 res += " by " 1294 res += " ".join(map(asFea, replacement)) 1295 res += ";" 1296 return res 1297 1298 1299class PairPosStatement(Statement): 1300 """A pair positioning statement. 1301 1302 ``glyphs1`` and ``glyphs2`` should be `glyph-containing objects`_. 1303 ``valuerecord1`` should be a :class:`ValueRecord` object; 1304 ``valuerecord2`` should be either a :class:`ValueRecord` object or ``None``. 1305 If ``enumerated`` is true, then this is expressed as an 1306 `enumerated pair <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#6.b.ii>`_. 1307 """ 1308 1309 def __init__( 1310 self, 1311 glyphs1, 1312 valuerecord1, 1313 glyphs2, 1314 valuerecord2, 1315 enumerated=False, 1316 location=None, 1317 ): 1318 Statement.__init__(self, location) 1319 self.enumerated = enumerated 1320 self.glyphs1, self.valuerecord1 = glyphs1, valuerecord1 1321 self.glyphs2, self.valuerecord2 = glyphs2, valuerecord2 1322 1323 def build(self, builder): 1324 """Calls a callback on the builder object: 1325 1326 * If the rule is enumerated, calls ``add_specific_pair_pos`` on each 1327 combination of first and second glyphs. 1328 * If the glyphs are both single :class:`GlyphName` objects, calls 1329 ``add_specific_pair_pos``. 1330 * Else, calls ``add_class_pair_pos``. 1331 """ 1332 if self.enumerated: 1333 g = [self.glyphs1.glyphSet(), self.glyphs2.glyphSet()] 1334 seen_pair = False 1335 for glyph1, glyph2 in itertools.product(*g): 1336 seen_pair = True 1337 builder.add_specific_pair_pos( 1338 self.location, glyph1, self.valuerecord1, glyph2, self.valuerecord2 1339 ) 1340 if not seen_pair: 1341 raise FeatureLibError( 1342 "Empty glyph class in positioning rule", self.location 1343 ) 1344 return 1345 1346 is_specific = isinstance(self.glyphs1, GlyphName) and isinstance( 1347 self.glyphs2, GlyphName 1348 ) 1349 if is_specific: 1350 builder.add_specific_pair_pos( 1351 self.location, 1352 self.glyphs1.glyph, 1353 self.valuerecord1, 1354 self.glyphs2.glyph, 1355 self.valuerecord2, 1356 ) 1357 else: 1358 builder.add_class_pair_pos( 1359 self.location, 1360 self.glyphs1.glyphSet(), 1361 self.valuerecord1, 1362 self.glyphs2.glyphSet(), 1363 self.valuerecord2, 1364 ) 1365 1366 def asFea(self, indent=""): 1367 res = "enum " if self.enumerated else "" 1368 if self.valuerecord2: 1369 res += "pos {} {} {} {};".format( 1370 self.glyphs1.asFea(), 1371 self.valuerecord1.asFea(), 1372 self.glyphs2.asFea(), 1373 self.valuerecord2.asFea(), 1374 ) 1375 else: 1376 res += "pos {} {} {};".format( 1377 self.glyphs1.asFea(), self.glyphs2.asFea(), self.valuerecord1.asFea() 1378 ) 1379 return res 1380 1381 1382class ReverseChainSingleSubstStatement(Statement): 1383 """A reverse chaining substitution statement. You don't see those every day. 1384 1385 Note the unusual argument order: ``suffix`` comes `before` ``glyphs``. 1386 ``old_prefix``, ``old_suffix``, ``glyphs`` and ``replacements`` should be 1387 lists of `glyph-containing objects`_. ``glyphs`` and ``replacements`` should 1388 be one-item lists. 1389 """ 1390 1391 def __init__(self, old_prefix, old_suffix, glyphs, replacements, location=None): 1392 Statement.__init__(self, location) 1393 self.old_prefix, self.old_suffix = old_prefix, old_suffix 1394 self.glyphs = glyphs 1395 self.replacements = replacements 1396 1397 def build(self, builder): 1398 prefix = [p.glyphSet() for p in self.old_prefix] 1399 suffix = [s.glyphSet() for s in self.old_suffix] 1400 originals = self.glyphs[0].glyphSet() 1401 replaces = self.replacements[0].glyphSet() 1402 if len(replaces) == 1: 1403 replaces = replaces * len(originals) 1404 builder.add_reverse_chain_single_subst( 1405 self.location, prefix, suffix, dict(zip(originals, replaces)) 1406 ) 1407 1408 def asFea(self, indent=""): 1409 res = "rsub " 1410 if len(self.old_prefix) or len(self.old_suffix): 1411 if len(self.old_prefix): 1412 res += " ".join(asFea(g) for g in self.old_prefix) + " " 1413 res += " ".join(asFea(g) + "'" for g in self.glyphs) 1414 if len(self.old_suffix): 1415 res += " " + " ".join(asFea(g) for g in self.old_suffix) 1416 else: 1417 res += " ".join(map(asFea, self.glyphs)) 1418 res += " by {};".format(" ".join(asFea(g) for g in self.replacements)) 1419 return res 1420 1421 1422class SingleSubstStatement(Statement): 1423 """A single substitution statement. 1424 1425 Note the unusual argument order: ``prefix`` and suffix come `after` 1426 the replacement ``glyphs``. ``prefix``, ``suffix``, ``glyphs`` and 1427 ``replace`` should be lists of `glyph-containing objects`_. ``glyphs`` and 1428 ``replace`` should be one-item lists. 1429 """ 1430 1431 def __init__(self, glyphs, replace, prefix, suffix, forceChain, location=None): 1432 Statement.__init__(self, location) 1433 self.prefix, self.suffix = prefix, suffix 1434 self.forceChain = forceChain 1435 self.glyphs = glyphs 1436 self.replacements = replace 1437 1438 def build(self, builder): 1439 """Calls the builder object's ``add_single_subst`` callback.""" 1440 prefix = [p.glyphSet() for p in self.prefix] 1441 suffix = [s.glyphSet() for s in self.suffix] 1442 originals = self.glyphs[0].glyphSet() 1443 replaces = self.replacements[0].glyphSet() 1444 if len(replaces) == 1: 1445 replaces = replaces * len(originals) 1446 builder.add_single_subst( 1447 self.location, 1448 prefix, 1449 suffix, 1450 OrderedDict(zip(originals, replaces)), 1451 self.forceChain, 1452 ) 1453 1454 def asFea(self, indent=""): 1455 res = "sub " 1456 if len(self.prefix) or len(self.suffix) or self.forceChain: 1457 if len(self.prefix): 1458 res += " ".join(asFea(g) for g in self.prefix) + " " 1459 res += " ".join(asFea(g) + "'" for g in self.glyphs) 1460 if len(self.suffix): 1461 res += " " + " ".join(asFea(g) for g in self.suffix) 1462 else: 1463 res += " ".join(asFea(g) for g in self.glyphs) 1464 res += " by {};".format(" ".join(asFea(g) for g in self.replacements)) 1465 return res 1466 1467 1468class ScriptStatement(Statement): 1469 """A ``script`` statement.""" 1470 1471 def __init__(self, script, location=None): 1472 Statement.__init__(self, location) 1473 self.script = script #: the script code 1474 1475 def build(self, builder): 1476 """Calls the builder's ``set_script`` callback.""" 1477 builder.set_script(self.location, self.script) 1478 1479 def asFea(self, indent=""): 1480 return "script {};".format(self.script.strip()) 1481 1482 1483class SinglePosStatement(Statement): 1484 """A single position statement. ``prefix`` and ``suffix`` should be 1485 lists of `glyph-containing objects`_. 1486 1487 ``pos`` should be a one-element list containing a (`glyph-containing object`_, 1488 :class:`ValueRecord`) tuple.""" 1489 1490 def __init__(self, pos, prefix, suffix, forceChain, location=None): 1491 Statement.__init__(self, location) 1492 self.pos, self.prefix, self.suffix = pos, prefix, suffix 1493 self.forceChain = forceChain 1494 1495 def build(self, builder): 1496 """Calls the builder object's ``add_single_pos`` callback.""" 1497 prefix = [p.glyphSet() for p in self.prefix] 1498 suffix = [s.glyphSet() for s in self.suffix] 1499 pos = [(g.glyphSet(), value) for g, value in self.pos] 1500 builder.add_single_pos(self.location, prefix, suffix, pos, self.forceChain) 1501 1502 def asFea(self, indent=""): 1503 res = "pos " 1504 if len(self.prefix) or len(self.suffix) or self.forceChain: 1505 if len(self.prefix): 1506 res += " ".join(map(asFea, self.prefix)) + " " 1507 res += " ".join( 1508 [ 1509 asFea(x[0]) + "'" + ((" " + x[1].asFea()) if x[1] else "") 1510 for x in self.pos 1511 ] 1512 ) 1513 if len(self.suffix): 1514 res += " " + " ".join(map(asFea, self.suffix)) 1515 else: 1516 res += " ".join( 1517 [asFea(x[0]) + " " + (x[1].asFea() if x[1] else "") for x in self.pos] 1518 ) 1519 res += ";" 1520 return res 1521 1522 1523class SubtableStatement(Statement): 1524 """Represents a subtable break.""" 1525 1526 def __init__(self, location=None): 1527 Statement.__init__(self, location) 1528 1529 def build(self, builder): 1530 """Calls the builder objects's ``add_subtable_break`` callback.""" 1531 builder.add_subtable_break(self.location) 1532 1533 def asFea(self, indent=""): 1534 return "subtable;" 1535 1536 1537class ValueRecord(Expression): 1538 """Represents a value record.""" 1539 1540 def __init__( 1541 self, 1542 xPlacement=None, 1543 yPlacement=None, 1544 xAdvance=None, 1545 yAdvance=None, 1546 xPlaDevice=None, 1547 yPlaDevice=None, 1548 xAdvDevice=None, 1549 yAdvDevice=None, 1550 vertical=False, 1551 location=None, 1552 ): 1553 Expression.__init__(self, location) 1554 self.xPlacement, self.yPlacement = (xPlacement, yPlacement) 1555 self.xAdvance, self.yAdvance = (xAdvance, yAdvance) 1556 self.xPlaDevice, self.yPlaDevice = (xPlaDevice, yPlaDevice) 1557 self.xAdvDevice, self.yAdvDevice = (xAdvDevice, yAdvDevice) 1558 self.vertical = vertical 1559 1560 def __eq__(self, other): 1561 return ( 1562 self.xPlacement == other.xPlacement 1563 and self.yPlacement == other.yPlacement 1564 and self.xAdvance == other.xAdvance 1565 and self.yAdvance == other.yAdvance 1566 and self.xPlaDevice == other.xPlaDevice 1567 and self.xAdvDevice == other.xAdvDevice 1568 ) 1569 1570 def __ne__(self, other): 1571 return not self.__eq__(other) 1572 1573 def __hash__(self): 1574 return ( 1575 hash(self.xPlacement) 1576 ^ hash(self.yPlacement) 1577 ^ hash(self.xAdvance) 1578 ^ hash(self.yAdvance) 1579 ^ hash(self.xPlaDevice) 1580 ^ hash(self.yPlaDevice) 1581 ^ hash(self.xAdvDevice) 1582 ^ hash(self.yAdvDevice) 1583 ) 1584 1585 def asFea(self, indent=""): 1586 if not self: 1587 return "<NULL>" 1588 1589 x, y = self.xPlacement, self.yPlacement 1590 xAdvance, yAdvance = self.xAdvance, self.yAdvance 1591 xPlaDevice, yPlaDevice = self.xPlaDevice, self.yPlaDevice 1592 xAdvDevice, yAdvDevice = self.xAdvDevice, self.yAdvDevice 1593 vertical = self.vertical 1594 1595 # Try format A, if possible. 1596 if x is None and y is None: 1597 if xAdvance is None and vertical: 1598 return str(yAdvance) 1599 elif yAdvance is None and not vertical: 1600 return str(xAdvance) 1601 1602 # Make any remaining None value 0 to avoid generating invalid records. 1603 x = x or 0 1604 y = y or 0 1605 xAdvance = xAdvance or 0 1606 yAdvance = yAdvance or 0 1607 1608 # Try format B, if possible. 1609 if ( 1610 xPlaDevice is None 1611 and yPlaDevice is None 1612 and xAdvDevice is None 1613 and yAdvDevice is None 1614 ): 1615 return "<%s %s %s %s>" % (x, y, xAdvance, yAdvance) 1616 1617 # Last resort is format C. 1618 return "<%s %s %s %s %s %s %s %s>" % ( 1619 x, 1620 y, 1621 xAdvance, 1622 yAdvance, 1623 deviceToString(xPlaDevice), 1624 deviceToString(yPlaDevice), 1625 deviceToString(xAdvDevice), 1626 deviceToString(yAdvDevice), 1627 ) 1628 1629 def __bool__(self): 1630 return any( 1631 getattr(self, v) is not None 1632 for v in [ 1633 "xPlacement", 1634 "yPlacement", 1635 "xAdvance", 1636 "yAdvance", 1637 "xPlaDevice", 1638 "yPlaDevice", 1639 "xAdvDevice", 1640 "yAdvDevice", 1641 ] 1642 ) 1643 1644 __nonzero__ = __bool__ 1645 1646 1647class ValueRecordDefinition(Statement): 1648 """Represents a named value record definition.""" 1649 1650 def __init__(self, name, value, location=None): 1651 Statement.__init__(self, location) 1652 self.name = name #: Value record name as string 1653 self.value = value #: :class:`ValueRecord` object 1654 1655 def asFea(self, indent=""): 1656 return "valueRecordDef {} {};".format(self.value.asFea(), self.name) 1657 1658 1659def simplify_name_attributes(pid, eid, lid): 1660 if pid == 3 and eid == 1 and lid == 1033: 1661 return "" 1662 elif pid == 1 and eid == 0 and lid == 0: 1663 return "1" 1664 else: 1665 return "{} {} {}".format(pid, eid, lid) 1666 1667 1668class NameRecord(Statement): 1669 """Represents a name record. (`Section 9.e. <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.e>`_)""" 1670 1671 def __init__(self, nameID, platformID, platEncID, langID, string, location=None): 1672 Statement.__init__(self, location) 1673 self.nameID = nameID #: Name ID as integer (e.g. 9 for designer's name) 1674 self.platformID = platformID #: Platform ID as integer 1675 self.platEncID = platEncID #: Platform encoding ID as integer 1676 self.langID = langID #: Language ID as integer 1677 self.string = string #: Name record value 1678 1679 def build(self, builder): 1680 """Calls the builder object's ``add_name_record`` callback.""" 1681 builder.add_name_record( 1682 self.location, 1683 self.nameID, 1684 self.platformID, 1685 self.platEncID, 1686 self.langID, 1687 self.string, 1688 ) 1689 1690 def asFea(self, indent=""): 1691 def escape(c, escape_pattern): 1692 # Also escape U+0022 QUOTATION MARK and U+005C REVERSE SOLIDUS 1693 if c >= 0x20 and c <= 0x7E and c not in (0x22, 0x5C): 1694 return chr(c) 1695 else: 1696 return escape_pattern % c 1697 1698 encoding = getEncoding(self.platformID, self.platEncID, self.langID) 1699 if encoding is None: 1700 raise FeatureLibError("Unsupported encoding", self.location) 1701 s = tobytes(self.string, encoding=encoding) 1702 if encoding == "utf_16_be": 1703 escaped_string = "".join( 1704 [ 1705 escape(byteord(s[i]) * 256 + byteord(s[i + 1]), r"\%04x") 1706 for i in range(0, len(s), 2) 1707 ] 1708 ) 1709 else: 1710 escaped_string = "".join([escape(byteord(b), r"\%02x") for b in s]) 1711 plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID) 1712 if plat != "": 1713 plat += " " 1714 return 'nameid {} {}"{}";'.format(self.nameID, plat, escaped_string) 1715 1716 1717class FeatureNameStatement(NameRecord): 1718 """Represents a ``sizemenuname`` or ``name`` statement.""" 1719 1720 def build(self, builder): 1721 """Calls the builder object's ``add_featureName`` callback.""" 1722 NameRecord.build(self, builder) 1723 builder.add_featureName(self.nameID) 1724 1725 def asFea(self, indent=""): 1726 if self.nameID == "size": 1727 tag = "sizemenuname" 1728 else: 1729 tag = "name" 1730 plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID) 1731 if plat != "": 1732 plat += " " 1733 return '{} {}"{}";'.format(tag, plat, self.string) 1734 1735 1736class STATNameStatement(NameRecord): 1737 """Represents a STAT table ``name`` statement.""" 1738 1739 def asFea(self, indent=""): 1740 plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID) 1741 if plat != "": 1742 plat += " " 1743 return 'name {}"{}";'.format(plat, self.string) 1744 1745 1746class SizeParameters(Statement): 1747 """A ``parameters`` statement.""" 1748 1749 def __init__(self, DesignSize, SubfamilyID, RangeStart, RangeEnd, location=None): 1750 Statement.__init__(self, location) 1751 self.DesignSize = DesignSize 1752 self.SubfamilyID = SubfamilyID 1753 self.RangeStart = RangeStart 1754 self.RangeEnd = RangeEnd 1755 1756 def build(self, builder): 1757 """Calls the builder object's ``set_size_parameters`` callback.""" 1758 builder.set_size_parameters( 1759 self.location, 1760 self.DesignSize, 1761 self.SubfamilyID, 1762 self.RangeStart, 1763 self.RangeEnd, 1764 ) 1765 1766 def asFea(self, indent=""): 1767 res = "parameters {:.1f} {}".format(self.DesignSize, self.SubfamilyID) 1768 if self.RangeStart != 0 or self.RangeEnd != 0: 1769 res += " {} {}".format(int(self.RangeStart * 10), int(self.RangeEnd * 10)) 1770 return res + ";" 1771 1772 1773class CVParametersNameStatement(NameRecord): 1774 """Represent a name statement inside a ``cvParameters`` block.""" 1775 1776 def __init__( 1777 self, nameID, platformID, platEncID, langID, string, block_name, location=None 1778 ): 1779 NameRecord.__init__( 1780 self, nameID, platformID, platEncID, langID, string, location=location 1781 ) 1782 self.block_name = block_name 1783 1784 def build(self, builder): 1785 """Calls the builder object's ``add_cv_parameter`` callback.""" 1786 item = "" 1787 if self.block_name == "ParamUILabelNameID": 1788 item = "_{}".format(builder.cv_num_named_params_.get(self.nameID, 0)) 1789 builder.add_cv_parameter(self.nameID) 1790 self.nameID = (self.nameID, self.block_name + item) 1791 NameRecord.build(self, builder) 1792 1793 def asFea(self, indent=""): 1794 plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID) 1795 if plat != "": 1796 plat += " " 1797 return 'name {}"{}";'.format(plat, self.string) 1798 1799 1800class CharacterStatement(Statement): 1801 """ 1802 Statement used in cvParameters blocks of Character Variant features (cvXX). 1803 The Unicode value may be written with either decimal or hexadecimal 1804 notation. The value must be preceded by '0x' if it is a hexadecimal value. 1805 The largest Unicode value allowed is 0xFFFFFF. 1806 """ 1807 1808 def __init__(self, character, tag, location=None): 1809 Statement.__init__(self, location) 1810 self.character = character 1811 self.tag = tag 1812 1813 def build(self, builder): 1814 """Calls the builder object's ``add_cv_character`` callback.""" 1815 builder.add_cv_character(self.character, self.tag) 1816 1817 def asFea(self, indent=""): 1818 return "Character {:#x};".format(self.character) 1819 1820 1821class BaseAxis(Statement): 1822 """An axis definition, being either a ``VertAxis.BaseTagList/BaseScriptList`` 1823 pair or a ``HorizAxis.BaseTagList/BaseScriptList`` pair.""" 1824 1825 def __init__(self, bases, scripts, vertical, location=None): 1826 Statement.__init__(self, location) 1827 self.bases = bases #: A list of baseline tag names as strings 1828 self.scripts = scripts #: A list of script record tuplets (script tag, default baseline tag, base coordinate) 1829 self.vertical = vertical #: Boolean; VertAxis if True, HorizAxis if False 1830 1831 def build(self, builder): 1832 """Calls the builder object's ``set_base_axis`` callback.""" 1833 builder.set_base_axis(self.bases, self.scripts, self.vertical) 1834 1835 def asFea(self, indent=""): 1836 direction = "Vert" if self.vertical else "Horiz" 1837 scripts = [ 1838 "{} {} {}".format(a[0], a[1], " ".join(map(str, a[2]))) 1839 for a in self.scripts 1840 ] 1841 return "{}Axis.BaseTagList {};\n{}{}Axis.BaseScriptList {};".format( 1842 direction, " ".join(self.bases), indent, direction, ", ".join(scripts) 1843 ) 1844 1845 1846class OS2Field(Statement): 1847 """An entry in the ``OS/2`` table. Most ``values`` should be numbers or 1848 strings, apart from when the key is ``UnicodeRange``, ``CodePageRange`` 1849 or ``Panose``, in which case it should be an array of integers.""" 1850 1851 def __init__(self, key, value, location=None): 1852 Statement.__init__(self, location) 1853 self.key = key 1854 self.value = value 1855 1856 def build(self, builder): 1857 """Calls the builder object's ``add_os2_field`` callback.""" 1858 builder.add_os2_field(self.key, self.value) 1859 1860 def asFea(self, indent=""): 1861 def intarr2str(x): 1862 return " ".join(map(str, x)) 1863 1864 numbers = ( 1865 "FSType", 1866 "TypoAscender", 1867 "TypoDescender", 1868 "TypoLineGap", 1869 "winAscent", 1870 "winDescent", 1871 "XHeight", 1872 "CapHeight", 1873 "WeightClass", 1874 "WidthClass", 1875 "LowerOpSize", 1876 "UpperOpSize", 1877 ) 1878 ranges = ("UnicodeRange", "CodePageRange") 1879 keywords = dict([(x.lower(), [x, str]) for x in numbers]) 1880 keywords.update([(x.lower(), [x, intarr2str]) for x in ranges]) 1881 keywords["panose"] = ["Panose", intarr2str] 1882 keywords["vendor"] = ["Vendor", lambda y: '"{}"'.format(y)] 1883 if self.key in keywords: 1884 return "{} {};".format( 1885 keywords[self.key][0], keywords[self.key][1](self.value) 1886 ) 1887 return "" # should raise exception 1888 1889 1890class HheaField(Statement): 1891 """An entry in the ``hhea`` table.""" 1892 1893 def __init__(self, key, value, location=None): 1894 Statement.__init__(self, location) 1895 self.key = key 1896 self.value = value 1897 1898 def build(self, builder): 1899 """Calls the builder object's ``add_hhea_field`` callback.""" 1900 builder.add_hhea_field(self.key, self.value) 1901 1902 def asFea(self, indent=""): 1903 fields = ("CaretOffset", "Ascender", "Descender", "LineGap") 1904 keywords = dict([(x.lower(), x) for x in fields]) 1905 return "{} {};".format(keywords[self.key], self.value) 1906 1907 1908class VheaField(Statement): 1909 """An entry in the ``vhea`` table.""" 1910 1911 def __init__(self, key, value, location=None): 1912 Statement.__init__(self, location) 1913 self.key = key 1914 self.value = value 1915 1916 def build(self, builder): 1917 """Calls the builder object's ``add_vhea_field`` callback.""" 1918 builder.add_vhea_field(self.key, self.value) 1919 1920 def asFea(self, indent=""): 1921 fields = ("VertTypoAscender", "VertTypoDescender", "VertTypoLineGap") 1922 keywords = dict([(x.lower(), x) for x in fields]) 1923 return "{} {};".format(keywords[self.key], self.value) 1924 1925 1926class STATDesignAxisStatement(Statement): 1927 """A STAT table Design Axis 1928 1929 Args: 1930 tag (str): a 4 letter axis tag 1931 axisOrder (int): an int 1932 names (list): a list of :class:`STATNameStatement` objects 1933 """ 1934 1935 def __init__(self, tag, axisOrder, names, location=None): 1936 Statement.__init__(self, location) 1937 self.tag = tag 1938 self.axisOrder = axisOrder 1939 self.names = names 1940 self.location = location 1941 1942 def build(self, builder): 1943 builder.addDesignAxis(self, self.location) 1944 1945 def asFea(self, indent=""): 1946 indent += SHIFT 1947 res = f"DesignAxis {self.tag} {self.axisOrder} {{ \n" 1948 res += ("\n" + indent).join([s.asFea(indent=indent) for s in self.names]) + "\n" 1949 res += "};" 1950 return res 1951 1952 1953class ElidedFallbackName(Statement): 1954 """STAT table ElidedFallbackName 1955 1956 Args: 1957 names: a list of :class:`STATNameStatement` objects 1958 """ 1959 1960 def __init__(self, names, location=None): 1961 Statement.__init__(self, location) 1962 self.names = names 1963 self.location = location 1964 1965 def build(self, builder): 1966 builder.setElidedFallbackName(self.names, self.location) 1967 1968 def asFea(self, indent=""): 1969 indent += SHIFT 1970 res = "ElidedFallbackName { \n" 1971 res += ("\n" + indent).join([s.asFea(indent=indent) for s in self.names]) + "\n" 1972 res += "};" 1973 return res 1974 1975 1976class ElidedFallbackNameID(Statement): 1977 """STAT table ElidedFallbackNameID 1978 1979 Args: 1980 value: an int pointing to an existing name table name ID 1981 """ 1982 1983 def __init__(self, value, location=None): 1984 Statement.__init__(self, location) 1985 self.value = value 1986 self.location = location 1987 1988 def build(self, builder): 1989 builder.setElidedFallbackName(self.value, self.location) 1990 1991 def asFea(self, indent=""): 1992 return f"ElidedFallbackNameID {self.value};" 1993 1994 1995class STATAxisValueStatement(Statement): 1996 """A STAT table Axis Value Record 1997 1998 Args: 1999 names (list): a list of :class:`STATNameStatement` objects 2000 locations (list): a list of :class:`AxisValueLocationStatement` objects 2001 flags (int): an int 2002 """ 2003 2004 def __init__(self, names, locations, flags, location=None): 2005 Statement.__init__(self, location) 2006 self.names = names 2007 self.locations = locations 2008 self.flags = flags 2009 2010 def build(self, builder): 2011 builder.addAxisValueRecord(self, self.location) 2012 2013 def asFea(self, indent=""): 2014 res = "AxisValue {\n" 2015 for location in self.locations: 2016 res += location.asFea() 2017 2018 for nameRecord in self.names: 2019 res += nameRecord.asFea() 2020 res += "\n" 2021 2022 if self.flags: 2023 flags = ["OlderSiblingFontAttribute", "ElidableAxisValueName"] 2024 flagStrings = [] 2025 curr = 1 2026 for i in range(len(flags)): 2027 if self.flags & curr != 0: 2028 flagStrings.append(flags[i]) 2029 curr = curr << 1 2030 res += f"flag {' '.join(flagStrings)};\n" 2031 res += "};" 2032 return res 2033 2034 2035class AxisValueLocationStatement(Statement): 2036 """ 2037 A STAT table Axis Value Location 2038 2039 Args: 2040 tag (str): a 4 letter axis tag 2041 values (list): a list of ints and/or floats 2042 """ 2043 2044 def __init__(self, tag, values, location=None): 2045 Statement.__init__(self, location) 2046 self.tag = tag 2047 self.values = values 2048 2049 def asFea(self, res=""): 2050 res += f"location {self.tag} " 2051 res += f"{' '.join(str(i) for i in self.values)};\n" 2052 return res 2053 2054 2055class ConditionsetStatement(Statement): 2056 """ 2057 A variable layout conditionset 2058 2059 Args: 2060 name (str): the name of this conditionset 2061 conditions (dict): a dictionary mapping axis tags to a 2062 tuple of (min,max) userspace coordinates. 2063 """ 2064 2065 def __init__(self, name, conditions, location=None): 2066 Statement.__init__(self, location) 2067 self.name = name 2068 self.conditions = conditions 2069 2070 def build(self, builder): 2071 builder.add_conditionset(self.name, self.conditions) 2072 2073 def asFea(self, res="", indent=""): 2074 res += indent + f"conditionset {self.name} " + "{\n" 2075 for tag, (minvalue, maxvalue) in self.conditions.items(): 2076 res += indent + SHIFT + f"{tag} {minvalue} {maxvalue};\n" 2077 res += indent + "}" + f" {self.name};\n" 2078 return res 2079 2080 2081class VariationBlock(Block): 2082 """A variation feature block, applicable in a given set of conditions.""" 2083 2084 def __init__(self, name, conditionset, use_extension=False, location=None): 2085 Block.__init__(self, location) 2086 self.name, self.conditionset, self.use_extension = ( 2087 name, 2088 conditionset, 2089 use_extension, 2090 ) 2091 2092 def build(self, builder): 2093 """Call the ``start_feature`` callback on the builder object, visit 2094 all the statements in this feature, and then call ``end_feature``.""" 2095 builder.start_feature(self.location, self.name) 2096 if ( 2097 self.conditionset != "NULL" 2098 and self.conditionset not in builder.conditionsets_ 2099 ): 2100 raise FeatureLibError( 2101 f"variation block used undefined conditionset {self.conditionset}", 2102 self.location, 2103 ) 2104 2105 # language exclude_dflt statements modify builder.features_ 2106 # limit them to this block with temporary builder.features_ 2107 features = builder.features_ 2108 builder.features_ = {} 2109 Block.build(self, builder) 2110 for key, value in builder.features_.items(): 2111 items = builder.feature_variations_.setdefault(key, {}).setdefault( 2112 self.conditionset, [] 2113 ) 2114 items.extend(value) 2115 if key not in features: 2116 features[key] = [] # Ensure we make a feature record 2117 builder.features_ = features 2118 builder.end_feature() 2119 2120 def asFea(self, indent=""): 2121 res = indent + "variation %s " % self.name.strip() 2122 res += self.conditionset + " " 2123 if self.use_extension: 2124 res += "useExtension " 2125 res += "{\n" 2126 res += Block.asFea(self, indent=indent) 2127 res += indent + "} %s;\n" % self.name.strip() 2128 return res 2129