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): 916 res += " ".join(map(asFea, prefix)) + " " 917 res += " ".join(g.asFea() + "'" for g in glyphs) 918 if len(suffix): 919 res += " " + " ".join(map(asFea, suffix)) 920 contexts.append(res) 921 return "ignore sub " + ", ".join(contexts) + ";" 922 923 924class IncludeStatement(Statement): 925 """An ``include()`` statement.""" 926 927 def __init__(self, filename, location=None): 928 super(IncludeStatement, self).__init__(location) 929 self.filename = filename #: String containing name of file to include 930 931 def build(self): 932 # TODO: consider lazy-loading the including parser/lexer? 933 raise FeatureLibError( 934 "Building an include statement is not implemented yet. " 935 "Instead, use Parser(..., followIncludes=True) for building.", 936 self.location, 937 ) 938 939 def asFea(self, indent=""): 940 return indent + "include(%s);" % self.filename 941 942 943class LanguageStatement(Statement): 944 """A ``language`` statement within a feature.""" 945 946 def __init__(self, language, include_default=True, required=False, location=None): 947 Statement.__init__(self, location) 948 assert len(language) == 4 949 self.language = language #: A four-character language tag 950 self.include_default = include_default #: If false, "exclude_dflt" 951 self.required = required 952 953 def build(self, builder): 954 """Call the builder object's ``set_language`` callback.""" 955 builder.set_language( 956 location=self.location, 957 language=self.language, 958 include_default=self.include_default, 959 required=self.required, 960 ) 961 962 def asFea(self, indent=""): 963 res = "language {}".format(self.language.strip()) 964 if not self.include_default: 965 res += " exclude_dflt" 966 if self.required: 967 res += " required" 968 res += ";" 969 return res 970 971 972class LanguageSystemStatement(Statement): 973 """A top-level ``languagesystem`` statement.""" 974 975 def __init__(self, script, language, location=None): 976 Statement.__init__(self, location) 977 self.script, self.language = (script, language) 978 979 def build(self, builder): 980 """Calls the builder object's ``add_language_system`` callback.""" 981 builder.add_language_system(self.location, self.script, self.language) 982 983 def asFea(self, indent=""): 984 return "languagesystem {} {};".format(self.script, self.language.strip()) 985 986 987class FontRevisionStatement(Statement): 988 """A ``head`` table ``FontRevision`` statement. ``revision`` should be a 989 number, and will be formatted to three significant decimal places.""" 990 991 def __init__(self, revision, location=None): 992 Statement.__init__(self, location) 993 self.revision = revision 994 995 def build(self, builder): 996 builder.set_font_revision(self.location, self.revision) 997 998 def asFea(self, indent=""): 999 return "FontRevision {:.3f};".format(self.revision) 1000 1001 1002class LigatureCaretByIndexStatement(Statement): 1003 """A ``GDEF`` table ``LigatureCaretByIndex`` statement. ``glyphs`` should be 1004 a `glyph-containing object`_, and ``carets`` should be a list of integers.""" 1005 1006 def __init__(self, glyphs, carets, location=None): 1007 Statement.__init__(self, location) 1008 self.glyphs, self.carets = (glyphs, carets) 1009 1010 def build(self, builder): 1011 """Calls the builder object's ``add_ligatureCaretByIndex_`` callback.""" 1012 glyphs = self.glyphs.glyphSet() 1013 builder.add_ligatureCaretByIndex_(self.location, glyphs, set(self.carets)) 1014 1015 def asFea(self, indent=""): 1016 return "LigatureCaretByIndex {} {};".format( 1017 self.glyphs.asFea(), " ".join(str(x) for x in self.carets) 1018 ) 1019 1020 1021class LigatureCaretByPosStatement(Statement): 1022 """A ``GDEF`` table ``LigatureCaretByPos`` statement. ``glyphs`` should be 1023 a `glyph-containing object`_, and ``carets`` should be a list of integers.""" 1024 1025 def __init__(self, glyphs, carets, location=None): 1026 Statement.__init__(self, location) 1027 self.glyphs, self.carets = (glyphs, carets) 1028 1029 def build(self, builder): 1030 """Calls the builder object's ``add_ligatureCaretByPos_`` callback.""" 1031 glyphs = self.glyphs.glyphSet() 1032 builder.add_ligatureCaretByPos_(self.location, glyphs, set(self.carets)) 1033 1034 def asFea(self, indent=""): 1035 return "LigatureCaretByPos {} {};".format( 1036 self.glyphs.asFea(), " ".join(str(x) for x in self.carets) 1037 ) 1038 1039 1040class LigatureSubstStatement(Statement): 1041 """A chained contextual substitution statement. 1042 1043 ``prefix``, ``glyphs``, and ``suffix`` should be lists of 1044 `glyph-containing objects`_; ``replacement`` should be a single 1045 `glyph-containing object`_. 1046 1047 If ``forceChain`` is True, this is expressed as a chaining rule 1048 (e.g. ``sub f' i' by f_i``) even when no context is given.""" 1049 1050 def __init__(self, prefix, glyphs, suffix, replacement, forceChain, location=None): 1051 Statement.__init__(self, location) 1052 self.prefix, self.glyphs, self.suffix = (prefix, glyphs, suffix) 1053 self.replacement, self.forceChain = replacement, forceChain 1054 1055 def build(self, builder): 1056 prefix = [p.glyphSet() for p in self.prefix] 1057 glyphs = [g.glyphSet() for g in self.glyphs] 1058 suffix = [s.glyphSet() for s in self.suffix] 1059 builder.add_ligature_subst( 1060 self.location, prefix, glyphs, suffix, self.replacement, self.forceChain 1061 ) 1062 1063 def asFea(self, indent=""): 1064 res = "sub " 1065 if len(self.prefix) or len(self.suffix) or self.forceChain: 1066 if len(self.prefix): 1067 res += " ".join(g.asFea() for g in self.prefix) + " " 1068 res += " ".join(g.asFea() + "'" for g in self.glyphs) 1069 if len(self.suffix): 1070 res += " " + " ".join(g.asFea() for g in self.suffix) 1071 else: 1072 res += " ".join(g.asFea() for g in self.glyphs) 1073 res += " by " 1074 res += asFea(self.replacement) 1075 res += ";" 1076 return res 1077 1078 1079class LookupFlagStatement(Statement): 1080 """A ``lookupflag`` statement. The ``value`` should be an integer value 1081 representing the flags in use, but not including the ``markAttachment`` 1082 class and ``markFilteringSet`` values, which must be specified as 1083 glyph-containing objects.""" 1084 1085 def __init__( 1086 self, value=0, markAttachment=None, markFilteringSet=None, location=None 1087 ): 1088 Statement.__init__(self, location) 1089 self.value = value 1090 self.markAttachment = markAttachment 1091 self.markFilteringSet = markFilteringSet 1092 1093 def build(self, builder): 1094 """Calls the builder object's ``set_lookup_flag`` callback.""" 1095 markAttach = None 1096 if self.markAttachment is not None: 1097 markAttach = self.markAttachment.glyphSet() 1098 markFilter = None 1099 if self.markFilteringSet is not None: 1100 markFilter = self.markFilteringSet.glyphSet() 1101 builder.set_lookup_flag(self.location, self.value, markAttach, markFilter) 1102 1103 def asFea(self, indent=""): 1104 res = [] 1105 flags = ["RightToLeft", "IgnoreBaseGlyphs", "IgnoreLigatures", "IgnoreMarks"] 1106 curr = 1 1107 for i in range(len(flags)): 1108 if self.value & curr != 0: 1109 res.append(flags[i]) 1110 curr = curr << 1 1111 if self.markAttachment is not None: 1112 res.append("MarkAttachmentType {}".format(self.markAttachment.asFea())) 1113 if self.markFilteringSet is not None: 1114 res.append("UseMarkFilteringSet {}".format(self.markFilteringSet.asFea())) 1115 if not res: 1116 res = ["0"] 1117 return "lookupflag {};".format(" ".join(res)) 1118 1119 1120class LookupReferenceStatement(Statement): 1121 """Represents a ``lookup ...;`` statement to include a lookup in a feature. 1122 1123 The ``lookup`` should be a :class:`LookupBlock` object.""" 1124 1125 def __init__(self, lookup, location=None): 1126 Statement.__init__(self, location) 1127 self.location, self.lookup = (location, lookup) 1128 1129 def build(self, builder): 1130 """Calls the builder object's ``add_lookup_call`` callback.""" 1131 builder.add_lookup_call(self.lookup.name) 1132 1133 def asFea(self, indent=""): 1134 return "lookup {};".format(self.lookup.name) 1135 1136 1137class MarkBasePosStatement(Statement): 1138 """A mark-to-base positioning rule. The ``base`` should be a 1139 `glyph-containing object`_. The ``marks`` should be a list of 1140 (:class:`Anchor`, :class:`MarkClass`) tuples.""" 1141 1142 def __init__(self, base, marks, location=None): 1143 Statement.__init__(self, location) 1144 self.base, self.marks = base, marks 1145 1146 def build(self, builder): 1147 """Calls the builder object's ``add_mark_base_pos`` callback.""" 1148 builder.add_mark_base_pos(self.location, self.base.glyphSet(), self.marks) 1149 1150 def asFea(self, indent=""): 1151 res = "pos base {}".format(self.base.asFea()) 1152 for a, m in self.marks: 1153 res += "\n" + indent + SHIFT + "{} mark @{}".format(a.asFea(), m.name) 1154 res += ";" 1155 return res 1156 1157 1158class MarkLigPosStatement(Statement): 1159 """A mark-to-ligature positioning rule. The ``ligatures`` must be a 1160 `glyph-containing object`_. The ``marks`` should be a list of lists: each 1161 element in the top-level list represents a component glyph, and is made 1162 up of a list of (:class:`Anchor`, :class:`MarkClass`) tuples representing 1163 mark attachment points for that position. 1164 1165 Example:: 1166 1167 m1 = MarkClass("TOP_MARKS") 1168 m2 = MarkClass("BOTTOM_MARKS") 1169 # ... add definitions to mark classes... 1170 1171 glyph = GlyphName("lam_meem_jeem") 1172 marks = [ 1173 [ (Anchor(625,1800), m1) ], # Attachments on 1st component (lam) 1174 [ (Anchor(376,-378), m2) ], # Attachments on 2nd component (meem) 1175 [ ] # No attachments on the jeem 1176 ] 1177 mlp = MarkLigPosStatement(glyph, marks) 1178 1179 mlp.asFea() 1180 # pos ligature lam_meem_jeem <anchor 625 1800> mark @TOP_MARKS 1181 # ligComponent <anchor 376 -378> mark @BOTTOM_MARKS; 1182 1183 """ 1184 1185 def __init__(self, ligatures, marks, location=None): 1186 Statement.__init__(self, location) 1187 self.ligatures, self.marks = ligatures, marks 1188 1189 def build(self, builder): 1190 """Calls the builder object's ``add_mark_lig_pos`` callback.""" 1191 builder.add_mark_lig_pos(self.location, self.ligatures.glyphSet(), self.marks) 1192 1193 def asFea(self, indent=""): 1194 res = "pos ligature {}".format(self.ligatures.asFea()) 1195 ligs = [] 1196 for l in self.marks: 1197 temp = "" 1198 if l is None or not len(l): 1199 temp = "\n" + indent + SHIFT * 2 + "<anchor NULL>" 1200 else: 1201 for a, m in l: 1202 temp += ( 1203 "\n" 1204 + indent 1205 + SHIFT * 2 1206 + "{} mark @{}".format(a.asFea(), m.name) 1207 ) 1208 ligs.append(temp) 1209 res += ("\n" + indent + SHIFT + "ligComponent").join(ligs) 1210 res += ";" 1211 return res 1212 1213 1214class MarkMarkPosStatement(Statement): 1215 """A mark-to-mark positioning rule. The ``baseMarks`` must be a 1216 `glyph-containing object`_. The ``marks`` should be a list of 1217 (:class:`Anchor`, :class:`MarkClass`) tuples.""" 1218 1219 def __init__(self, baseMarks, marks, location=None): 1220 Statement.__init__(self, location) 1221 self.baseMarks, self.marks = baseMarks, marks 1222 1223 def build(self, builder): 1224 """Calls the builder object's ``add_mark_mark_pos`` callback.""" 1225 builder.add_mark_mark_pos(self.location, self.baseMarks.glyphSet(), self.marks) 1226 1227 def asFea(self, indent=""): 1228 res = "pos mark {}".format(self.baseMarks.asFea()) 1229 for a, m in self.marks: 1230 res += "\n" + indent + SHIFT + "{} mark @{}".format(a.asFea(), m.name) 1231 res += ";" 1232 return res 1233 1234 1235class MultipleSubstStatement(Statement): 1236 """A multiple substitution statement. 1237 1238 Args: 1239 prefix: a list of `glyph-containing objects`_. 1240 glyph: a single glyph-containing object. 1241 suffix: a list of glyph-containing objects. 1242 replacement: a list of glyph-containing objects. 1243 forceChain: If true, the statement is expressed as a chaining rule 1244 (e.g. ``sub f' i' by f_i``) even when no context is given. 1245 """ 1246 1247 def __init__( 1248 self, prefix, glyph, suffix, replacement, forceChain=False, location=None 1249 ): 1250 Statement.__init__(self, location) 1251 self.prefix, self.glyph, self.suffix = prefix, glyph, suffix 1252 self.replacement = replacement 1253 self.forceChain = forceChain 1254 1255 def build(self, builder): 1256 """Calls the builder object's ``add_multiple_subst`` callback.""" 1257 prefix = [p.glyphSet() for p in self.prefix] 1258 suffix = [s.glyphSet() for s in self.suffix] 1259 if hasattr(self.glyph, "glyphSet"): 1260 originals = self.glyph.glyphSet() 1261 else: 1262 originals = [self.glyph] 1263 count = len(originals) 1264 replaces = [] 1265 for r in self.replacement: 1266 if hasattr(r, "glyphSet"): 1267 replace = r.glyphSet() 1268 else: 1269 replace = [r] 1270 if len(replace) == 1 and len(replace) != count: 1271 replace = replace * count 1272 replaces.append(replace) 1273 replaces = list(zip(*replaces)) 1274 1275 seen_originals = set() 1276 for i, original in enumerate(originals): 1277 if original not in seen_originals: 1278 seen_originals.add(original) 1279 builder.add_multiple_subst( 1280 self.location, 1281 prefix, 1282 original, 1283 suffix, 1284 replaces and replaces[i] or (), 1285 self.forceChain, 1286 ) 1287 1288 def asFea(self, indent=""): 1289 res = "sub " 1290 if len(self.prefix) or len(self.suffix) or self.forceChain: 1291 if len(self.prefix): 1292 res += " ".join(map(asFea, self.prefix)) + " " 1293 res += asFea(self.glyph) + "'" 1294 if len(self.suffix): 1295 res += " " + " ".join(map(asFea, self.suffix)) 1296 else: 1297 res += asFea(self.glyph) 1298 replacement = self.replacement or [NullGlyph()] 1299 res += " by " 1300 res += " ".join(map(asFea, replacement)) 1301 res += ";" 1302 return res 1303 1304 1305class PairPosStatement(Statement): 1306 """A pair positioning statement. 1307 1308 ``glyphs1`` and ``glyphs2`` should be `glyph-containing objects`_. 1309 ``valuerecord1`` should be a :class:`ValueRecord` object; 1310 ``valuerecord2`` should be either a :class:`ValueRecord` object or ``None``. 1311 If ``enumerated`` is true, then this is expressed as an 1312 `enumerated pair <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#6.b.ii>`_. 1313 """ 1314 1315 def __init__( 1316 self, 1317 glyphs1, 1318 valuerecord1, 1319 glyphs2, 1320 valuerecord2, 1321 enumerated=False, 1322 location=None, 1323 ): 1324 Statement.__init__(self, location) 1325 self.enumerated = enumerated 1326 self.glyphs1, self.valuerecord1 = glyphs1, valuerecord1 1327 self.glyphs2, self.valuerecord2 = glyphs2, valuerecord2 1328 1329 def build(self, builder): 1330 """Calls a callback on the builder object: 1331 1332 * If the rule is enumerated, calls ``add_specific_pair_pos`` on each 1333 combination of first and second glyphs. 1334 * If the glyphs are both single :class:`GlyphName` objects, calls 1335 ``add_specific_pair_pos``. 1336 * Else, calls ``add_class_pair_pos``. 1337 """ 1338 if self.enumerated: 1339 g = [self.glyphs1.glyphSet(), self.glyphs2.glyphSet()] 1340 seen_pair = False 1341 for glyph1, glyph2 in itertools.product(*g): 1342 seen_pair = True 1343 builder.add_specific_pair_pos( 1344 self.location, glyph1, self.valuerecord1, glyph2, self.valuerecord2 1345 ) 1346 if not seen_pair: 1347 raise FeatureLibError( 1348 "Empty glyph class in positioning rule", self.location 1349 ) 1350 return 1351 1352 is_specific = isinstance(self.glyphs1, GlyphName) and isinstance( 1353 self.glyphs2, GlyphName 1354 ) 1355 if is_specific: 1356 builder.add_specific_pair_pos( 1357 self.location, 1358 self.glyphs1.glyph, 1359 self.valuerecord1, 1360 self.glyphs2.glyph, 1361 self.valuerecord2, 1362 ) 1363 else: 1364 builder.add_class_pair_pos( 1365 self.location, 1366 self.glyphs1.glyphSet(), 1367 self.valuerecord1, 1368 self.glyphs2.glyphSet(), 1369 self.valuerecord2, 1370 ) 1371 1372 def asFea(self, indent=""): 1373 res = "enum " if self.enumerated else "" 1374 if self.valuerecord2: 1375 res += "pos {} {} {} {};".format( 1376 self.glyphs1.asFea(), 1377 self.valuerecord1.asFea(), 1378 self.glyphs2.asFea(), 1379 self.valuerecord2.asFea(), 1380 ) 1381 else: 1382 res += "pos {} {} {};".format( 1383 self.glyphs1.asFea(), self.glyphs2.asFea(), self.valuerecord1.asFea() 1384 ) 1385 return res 1386 1387 1388class ReverseChainSingleSubstStatement(Statement): 1389 """A reverse chaining substitution statement. You don't see those every day. 1390 1391 Note the unusual argument order: ``suffix`` comes `before` ``glyphs``. 1392 ``old_prefix``, ``old_suffix``, ``glyphs`` and ``replacements`` should be 1393 lists of `glyph-containing objects`_. ``glyphs`` and ``replacements`` should 1394 be one-item lists. 1395 """ 1396 1397 def __init__(self, old_prefix, old_suffix, glyphs, replacements, location=None): 1398 Statement.__init__(self, location) 1399 self.old_prefix, self.old_suffix = old_prefix, old_suffix 1400 self.glyphs = glyphs 1401 self.replacements = replacements 1402 1403 def build(self, builder): 1404 prefix = [p.glyphSet() for p in self.old_prefix] 1405 suffix = [s.glyphSet() for s in self.old_suffix] 1406 originals = self.glyphs[0].glyphSet() 1407 replaces = self.replacements[0].glyphSet() 1408 if len(replaces) == 1: 1409 replaces = replaces * len(originals) 1410 builder.add_reverse_chain_single_subst( 1411 self.location, prefix, suffix, dict(zip(originals, replaces)) 1412 ) 1413 1414 def asFea(self, indent=""): 1415 res = "rsub " 1416 if len(self.old_prefix) or len(self.old_suffix): 1417 if len(self.old_prefix): 1418 res += " ".join(asFea(g) for g in self.old_prefix) + " " 1419 res += " ".join(asFea(g) + "'" for g in self.glyphs) 1420 if len(self.old_suffix): 1421 res += " " + " ".join(asFea(g) for g in self.old_suffix) 1422 else: 1423 res += " ".join(map(asFea, self.glyphs)) 1424 res += " by {};".format(" ".join(asFea(g) for g in self.replacements)) 1425 return res 1426 1427 1428class SingleSubstStatement(Statement): 1429 """A single substitution statement. 1430 1431 Note the unusual argument order: ``prefix`` and suffix come `after` 1432 the replacement ``glyphs``. ``prefix``, ``suffix``, ``glyphs`` and 1433 ``replace`` should be lists of `glyph-containing objects`_. ``glyphs`` and 1434 ``replace`` should be one-item lists. 1435 """ 1436 1437 def __init__(self, glyphs, replace, prefix, suffix, forceChain, location=None): 1438 Statement.__init__(self, location) 1439 self.prefix, self.suffix = prefix, suffix 1440 self.forceChain = forceChain 1441 self.glyphs = glyphs 1442 self.replacements = replace 1443 1444 def build(self, builder): 1445 """Calls the builder object's ``add_single_subst`` callback.""" 1446 prefix = [p.glyphSet() for p in self.prefix] 1447 suffix = [s.glyphSet() for s in self.suffix] 1448 originals = self.glyphs[0].glyphSet() 1449 replaces = self.replacements[0].glyphSet() 1450 if len(replaces) == 1: 1451 replaces = replaces * len(originals) 1452 builder.add_single_subst( 1453 self.location, 1454 prefix, 1455 suffix, 1456 OrderedDict(zip(originals, replaces)), 1457 self.forceChain, 1458 ) 1459 1460 def asFea(self, indent=""): 1461 res = "sub " 1462 if len(self.prefix) or len(self.suffix) or self.forceChain: 1463 if len(self.prefix): 1464 res += " ".join(asFea(g) for g in self.prefix) + " " 1465 res += " ".join(asFea(g) + "'" for g in self.glyphs) 1466 if len(self.suffix): 1467 res += " " + " ".join(asFea(g) for g in self.suffix) 1468 else: 1469 res += " ".join(asFea(g) for g in self.glyphs) 1470 res += " by {};".format(" ".join(asFea(g) for g in self.replacements)) 1471 return res 1472 1473 1474class ScriptStatement(Statement): 1475 """A ``script`` statement.""" 1476 1477 def __init__(self, script, location=None): 1478 Statement.__init__(self, location) 1479 self.script = script #: the script code 1480 1481 def build(self, builder): 1482 """Calls the builder's ``set_script`` callback.""" 1483 builder.set_script(self.location, self.script) 1484 1485 def asFea(self, indent=""): 1486 return "script {};".format(self.script.strip()) 1487 1488 1489class SinglePosStatement(Statement): 1490 """A single position statement. ``prefix`` and ``suffix`` should be 1491 lists of `glyph-containing objects`_. 1492 1493 ``pos`` should be a one-element list containing a (`glyph-containing object`_, 1494 :class:`ValueRecord`) tuple.""" 1495 1496 def __init__(self, pos, prefix, suffix, forceChain, location=None): 1497 Statement.__init__(self, location) 1498 self.pos, self.prefix, self.suffix = pos, prefix, suffix 1499 self.forceChain = forceChain 1500 1501 def build(self, builder): 1502 """Calls the builder object's ``add_single_pos`` callback.""" 1503 prefix = [p.glyphSet() for p in self.prefix] 1504 suffix = [s.glyphSet() for s in self.suffix] 1505 pos = [(g.glyphSet(), value) for g, value in self.pos] 1506 builder.add_single_pos(self.location, prefix, suffix, pos, self.forceChain) 1507 1508 def asFea(self, indent=""): 1509 res = "pos " 1510 if len(self.prefix) or len(self.suffix) or self.forceChain: 1511 if len(self.prefix): 1512 res += " ".join(map(asFea, self.prefix)) + " " 1513 res += " ".join( 1514 [ 1515 asFea(x[0]) + "'" + ((" " + x[1].asFea()) if x[1] else "") 1516 for x in self.pos 1517 ] 1518 ) 1519 if len(self.suffix): 1520 res += " " + " ".join(map(asFea, self.suffix)) 1521 else: 1522 res += " ".join( 1523 [asFea(x[0]) + " " + (x[1].asFea() if x[1] else "") for x in self.pos] 1524 ) 1525 res += ";" 1526 return res 1527 1528 1529class SubtableStatement(Statement): 1530 """Represents a subtable break.""" 1531 1532 def __init__(self, location=None): 1533 Statement.__init__(self, location) 1534 1535 def build(self, builder): 1536 """Calls the builder objects's ``add_subtable_break`` callback.""" 1537 builder.add_subtable_break(self.location) 1538 1539 def asFea(self, indent=""): 1540 return "subtable;" 1541 1542 1543class ValueRecord(Expression): 1544 """Represents a value record.""" 1545 1546 def __init__( 1547 self, 1548 xPlacement=None, 1549 yPlacement=None, 1550 xAdvance=None, 1551 yAdvance=None, 1552 xPlaDevice=None, 1553 yPlaDevice=None, 1554 xAdvDevice=None, 1555 yAdvDevice=None, 1556 vertical=False, 1557 location=None, 1558 ): 1559 Expression.__init__(self, location) 1560 self.xPlacement, self.yPlacement = (xPlacement, yPlacement) 1561 self.xAdvance, self.yAdvance = (xAdvance, yAdvance) 1562 self.xPlaDevice, self.yPlaDevice = (xPlaDevice, yPlaDevice) 1563 self.xAdvDevice, self.yAdvDevice = (xAdvDevice, yAdvDevice) 1564 self.vertical = vertical 1565 1566 def __eq__(self, other): 1567 return ( 1568 self.xPlacement == other.xPlacement 1569 and self.yPlacement == other.yPlacement 1570 and self.xAdvance == other.xAdvance 1571 and self.yAdvance == other.yAdvance 1572 and self.xPlaDevice == other.xPlaDevice 1573 and self.xAdvDevice == other.xAdvDevice 1574 ) 1575 1576 def __ne__(self, other): 1577 return not self.__eq__(other) 1578 1579 def __hash__(self): 1580 return ( 1581 hash(self.xPlacement) 1582 ^ hash(self.yPlacement) 1583 ^ hash(self.xAdvance) 1584 ^ hash(self.yAdvance) 1585 ^ hash(self.xPlaDevice) 1586 ^ hash(self.yPlaDevice) 1587 ^ hash(self.xAdvDevice) 1588 ^ hash(self.yAdvDevice) 1589 ) 1590 1591 def asFea(self, indent=""): 1592 if not self: 1593 return "<NULL>" 1594 1595 x, y = self.xPlacement, self.yPlacement 1596 xAdvance, yAdvance = self.xAdvance, self.yAdvance 1597 xPlaDevice, yPlaDevice = self.xPlaDevice, self.yPlaDevice 1598 xAdvDevice, yAdvDevice = self.xAdvDevice, self.yAdvDevice 1599 vertical = self.vertical 1600 1601 # Try format A, if possible. 1602 if x is None and y is None: 1603 if xAdvance is None and vertical: 1604 return str(yAdvance) 1605 elif yAdvance is None and not vertical: 1606 return str(xAdvance) 1607 1608 # Make any remaining None value 0 to avoid generating invalid records. 1609 x = x or 0 1610 y = y or 0 1611 xAdvance = xAdvance or 0 1612 yAdvance = yAdvance or 0 1613 1614 # Try format B, if possible. 1615 if ( 1616 xPlaDevice is None 1617 and yPlaDevice is None 1618 and xAdvDevice is None 1619 and yAdvDevice is None 1620 ): 1621 return "<%s %s %s %s>" % (x, y, xAdvance, yAdvance) 1622 1623 # Last resort is format C. 1624 return "<%s %s %s %s %s %s %s %s>" % ( 1625 x, 1626 y, 1627 xAdvance, 1628 yAdvance, 1629 deviceToString(xPlaDevice), 1630 deviceToString(yPlaDevice), 1631 deviceToString(xAdvDevice), 1632 deviceToString(yAdvDevice), 1633 ) 1634 1635 def __bool__(self): 1636 return any( 1637 getattr(self, v) is not None 1638 for v in [ 1639 "xPlacement", 1640 "yPlacement", 1641 "xAdvance", 1642 "yAdvance", 1643 "xPlaDevice", 1644 "yPlaDevice", 1645 "xAdvDevice", 1646 "yAdvDevice", 1647 ] 1648 ) 1649 1650 __nonzero__ = __bool__ 1651 1652 1653class ValueRecordDefinition(Statement): 1654 """Represents a named value record definition.""" 1655 1656 def __init__(self, name, value, location=None): 1657 Statement.__init__(self, location) 1658 self.name = name #: Value record name as string 1659 self.value = value #: :class:`ValueRecord` object 1660 1661 def asFea(self, indent=""): 1662 return "valueRecordDef {} {};".format(self.value.asFea(), self.name) 1663 1664 1665def simplify_name_attributes(pid, eid, lid): 1666 if pid == 3 and eid == 1 and lid == 1033: 1667 return "" 1668 elif pid == 1 and eid == 0 and lid == 0: 1669 return "1" 1670 else: 1671 return "{} {} {}".format(pid, eid, lid) 1672 1673 1674class NameRecord(Statement): 1675 """Represents a name record. (`Section 9.e. <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.e>`_)""" 1676 1677 def __init__(self, nameID, platformID, platEncID, langID, string, location=None): 1678 Statement.__init__(self, location) 1679 self.nameID = nameID #: Name ID as integer (e.g. 9 for designer's name) 1680 self.platformID = platformID #: Platform ID as integer 1681 self.platEncID = platEncID #: Platform encoding ID as integer 1682 self.langID = langID #: Language ID as integer 1683 self.string = string #: Name record value 1684 1685 def build(self, builder): 1686 """Calls the builder object's ``add_name_record`` callback.""" 1687 builder.add_name_record( 1688 self.location, 1689 self.nameID, 1690 self.platformID, 1691 self.platEncID, 1692 self.langID, 1693 self.string, 1694 ) 1695 1696 def asFea(self, indent=""): 1697 def escape(c, escape_pattern): 1698 # Also escape U+0022 QUOTATION MARK and U+005C REVERSE SOLIDUS 1699 if c >= 0x20 and c <= 0x7E and c not in (0x22, 0x5C): 1700 return chr(c) 1701 else: 1702 return escape_pattern % c 1703 1704 encoding = getEncoding(self.platformID, self.platEncID, self.langID) 1705 if encoding is None: 1706 raise FeatureLibError("Unsupported encoding", self.location) 1707 s = tobytes(self.string, encoding=encoding) 1708 if encoding == "utf_16_be": 1709 escaped_string = "".join( 1710 [ 1711 escape(byteord(s[i]) * 256 + byteord(s[i + 1]), r"\%04x") 1712 for i in range(0, len(s), 2) 1713 ] 1714 ) 1715 else: 1716 escaped_string = "".join([escape(byteord(b), r"\%02x") for b in s]) 1717 plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID) 1718 if plat != "": 1719 plat += " " 1720 return 'nameid {} {}"{}";'.format(self.nameID, plat, escaped_string) 1721 1722 1723class FeatureNameStatement(NameRecord): 1724 """Represents a ``sizemenuname`` or ``name`` statement.""" 1725 1726 def build(self, builder): 1727 """Calls the builder object's ``add_featureName`` callback.""" 1728 NameRecord.build(self, builder) 1729 builder.add_featureName(self.nameID) 1730 1731 def asFea(self, indent=""): 1732 if self.nameID == "size": 1733 tag = "sizemenuname" 1734 else: 1735 tag = "name" 1736 plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID) 1737 if plat != "": 1738 plat += " " 1739 return '{} {}"{}";'.format(tag, plat, self.string) 1740 1741 1742class STATNameStatement(NameRecord): 1743 """Represents a STAT table ``name`` statement.""" 1744 1745 def asFea(self, indent=""): 1746 plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID) 1747 if plat != "": 1748 plat += " " 1749 return 'name {}"{}";'.format(plat, self.string) 1750 1751 1752class SizeParameters(Statement): 1753 """A ``parameters`` statement.""" 1754 1755 def __init__(self, DesignSize, SubfamilyID, RangeStart, RangeEnd, location=None): 1756 Statement.__init__(self, location) 1757 self.DesignSize = DesignSize 1758 self.SubfamilyID = SubfamilyID 1759 self.RangeStart = RangeStart 1760 self.RangeEnd = RangeEnd 1761 1762 def build(self, builder): 1763 """Calls the builder object's ``set_size_parameters`` callback.""" 1764 builder.set_size_parameters( 1765 self.location, 1766 self.DesignSize, 1767 self.SubfamilyID, 1768 self.RangeStart, 1769 self.RangeEnd, 1770 ) 1771 1772 def asFea(self, indent=""): 1773 res = "parameters {:.1f} {}".format(self.DesignSize, self.SubfamilyID) 1774 if self.RangeStart != 0 or self.RangeEnd != 0: 1775 res += " {} {}".format(int(self.RangeStart * 10), int(self.RangeEnd * 10)) 1776 return res + ";" 1777 1778 1779class CVParametersNameStatement(NameRecord): 1780 """Represent a name statement inside a ``cvParameters`` block.""" 1781 1782 def __init__( 1783 self, nameID, platformID, platEncID, langID, string, block_name, location=None 1784 ): 1785 NameRecord.__init__( 1786 self, nameID, platformID, platEncID, langID, string, location=location 1787 ) 1788 self.block_name = block_name 1789 1790 def build(self, builder): 1791 """Calls the builder object's ``add_cv_parameter`` callback.""" 1792 item = "" 1793 if self.block_name == "ParamUILabelNameID": 1794 item = "_{}".format(builder.cv_num_named_params_.get(self.nameID, 0)) 1795 builder.add_cv_parameter(self.nameID) 1796 self.nameID = (self.nameID, self.block_name + item) 1797 NameRecord.build(self, builder) 1798 1799 def asFea(self, indent=""): 1800 plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID) 1801 if plat != "": 1802 plat += " " 1803 return 'name {}"{}";'.format(plat, self.string) 1804 1805 1806class CharacterStatement(Statement): 1807 """ 1808 Statement used in cvParameters blocks of Character Variant features (cvXX). 1809 The Unicode value may be written with either decimal or hexadecimal 1810 notation. The value must be preceded by '0x' if it is a hexadecimal value. 1811 The largest Unicode value allowed is 0xFFFFFF. 1812 """ 1813 1814 def __init__(self, character, tag, location=None): 1815 Statement.__init__(self, location) 1816 self.character = character 1817 self.tag = tag 1818 1819 def build(self, builder): 1820 """Calls the builder object's ``add_cv_character`` callback.""" 1821 builder.add_cv_character(self.character, self.tag) 1822 1823 def asFea(self, indent=""): 1824 return "Character {:#x};".format(self.character) 1825 1826 1827class BaseAxis(Statement): 1828 """An axis definition, being either a ``VertAxis.BaseTagList/BaseScriptList`` 1829 pair or a ``HorizAxis.BaseTagList/BaseScriptList`` pair.""" 1830 1831 def __init__(self, bases, scripts, vertical, location=None): 1832 Statement.__init__(self, location) 1833 self.bases = bases #: A list of baseline tag names as strings 1834 self.scripts = scripts #: A list of script record tuplets (script tag, default baseline tag, base coordinate) 1835 self.vertical = vertical #: Boolean; VertAxis if True, HorizAxis if False 1836 1837 def build(self, builder): 1838 """Calls the builder object's ``set_base_axis`` callback.""" 1839 builder.set_base_axis(self.bases, self.scripts, self.vertical) 1840 1841 def asFea(self, indent=""): 1842 direction = "Vert" if self.vertical else "Horiz" 1843 scripts = [ 1844 "{} {} {}".format(a[0], a[1], " ".join(map(str, a[2]))) 1845 for a in self.scripts 1846 ] 1847 return "{}Axis.BaseTagList {};\n{}{}Axis.BaseScriptList {};".format( 1848 direction, " ".join(self.bases), indent, direction, ", ".join(scripts) 1849 ) 1850 1851 1852class OS2Field(Statement): 1853 """An entry in the ``OS/2`` table. Most ``values`` should be numbers or 1854 strings, apart from when the key is ``UnicodeRange``, ``CodePageRange`` 1855 or ``Panose``, in which case it should be an array of integers.""" 1856 1857 def __init__(self, key, value, location=None): 1858 Statement.__init__(self, location) 1859 self.key = key 1860 self.value = value 1861 1862 def build(self, builder): 1863 """Calls the builder object's ``add_os2_field`` callback.""" 1864 builder.add_os2_field(self.key, self.value) 1865 1866 def asFea(self, indent=""): 1867 def intarr2str(x): 1868 return " ".join(map(str, x)) 1869 1870 numbers = ( 1871 "FSType", 1872 "TypoAscender", 1873 "TypoDescender", 1874 "TypoLineGap", 1875 "winAscent", 1876 "winDescent", 1877 "XHeight", 1878 "CapHeight", 1879 "WeightClass", 1880 "WidthClass", 1881 "LowerOpSize", 1882 "UpperOpSize", 1883 ) 1884 ranges = ("UnicodeRange", "CodePageRange") 1885 keywords = dict([(x.lower(), [x, str]) for x in numbers]) 1886 keywords.update([(x.lower(), [x, intarr2str]) for x in ranges]) 1887 keywords["panose"] = ["Panose", intarr2str] 1888 keywords["vendor"] = ["Vendor", lambda y: '"{}"'.format(y)] 1889 if self.key in keywords: 1890 return "{} {};".format( 1891 keywords[self.key][0], keywords[self.key][1](self.value) 1892 ) 1893 return "" # should raise exception 1894 1895 1896class HheaField(Statement): 1897 """An entry in the ``hhea`` table.""" 1898 1899 def __init__(self, key, value, location=None): 1900 Statement.__init__(self, location) 1901 self.key = key 1902 self.value = value 1903 1904 def build(self, builder): 1905 """Calls the builder object's ``add_hhea_field`` callback.""" 1906 builder.add_hhea_field(self.key, self.value) 1907 1908 def asFea(self, indent=""): 1909 fields = ("CaretOffset", "Ascender", "Descender", "LineGap") 1910 keywords = dict([(x.lower(), x) for x in fields]) 1911 return "{} {};".format(keywords[self.key], self.value) 1912 1913 1914class VheaField(Statement): 1915 """An entry in the ``vhea`` table.""" 1916 1917 def __init__(self, key, value, location=None): 1918 Statement.__init__(self, location) 1919 self.key = key 1920 self.value = value 1921 1922 def build(self, builder): 1923 """Calls the builder object's ``add_vhea_field`` callback.""" 1924 builder.add_vhea_field(self.key, self.value) 1925 1926 def asFea(self, indent=""): 1927 fields = ("VertTypoAscender", "VertTypoDescender", "VertTypoLineGap") 1928 keywords = dict([(x.lower(), x) for x in fields]) 1929 return "{} {};".format(keywords[self.key], self.value) 1930 1931 1932class STATDesignAxisStatement(Statement): 1933 """A STAT table Design Axis 1934 1935 Args: 1936 tag (str): a 4 letter axis tag 1937 axisOrder (int): an int 1938 names (list): a list of :class:`STATNameStatement` objects 1939 """ 1940 1941 def __init__(self, tag, axisOrder, names, location=None): 1942 Statement.__init__(self, location) 1943 self.tag = tag 1944 self.axisOrder = axisOrder 1945 self.names = names 1946 self.location = location 1947 1948 def build(self, builder): 1949 builder.addDesignAxis(self, self.location) 1950 1951 def asFea(self, indent=""): 1952 indent += SHIFT 1953 res = f"DesignAxis {self.tag} {self.axisOrder} {{ \n" 1954 res += ("\n" + indent).join([s.asFea(indent=indent) for s in self.names]) + "\n" 1955 res += "};" 1956 return res 1957 1958 1959class ElidedFallbackName(Statement): 1960 """STAT table ElidedFallbackName 1961 1962 Args: 1963 names: a list of :class:`STATNameStatement` objects 1964 """ 1965 1966 def __init__(self, names, location=None): 1967 Statement.__init__(self, location) 1968 self.names = names 1969 self.location = location 1970 1971 def build(self, builder): 1972 builder.setElidedFallbackName(self.names, self.location) 1973 1974 def asFea(self, indent=""): 1975 indent += SHIFT 1976 res = "ElidedFallbackName { \n" 1977 res += ("\n" + indent).join([s.asFea(indent=indent) for s in self.names]) + "\n" 1978 res += "};" 1979 return res 1980 1981 1982class ElidedFallbackNameID(Statement): 1983 """STAT table ElidedFallbackNameID 1984 1985 Args: 1986 value: an int pointing to an existing name table name ID 1987 """ 1988 1989 def __init__(self, value, location=None): 1990 Statement.__init__(self, location) 1991 self.value = value 1992 self.location = location 1993 1994 def build(self, builder): 1995 builder.setElidedFallbackName(self.value, self.location) 1996 1997 def asFea(self, indent=""): 1998 return f"ElidedFallbackNameID {self.value};" 1999 2000 2001class STATAxisValueStatement(Statement): 2002 """A STAT table Axis Value Record 2003 2004 Args: 2005 names (list): a list of :class:`STATNameStatement` objects 2006 locations (list): a list of :class:`AxisValueLocationStatement` objects 2007 flags (int): an int 2008 """ 2009 2010 def __init__(self, names, locations, flags, location=None): 2011 Statement.__init__(self, location) 2012 self.names = names 2013 self.locations = locations 2014 self.flags = flags 2015 2016 def build(self, builder): 2017 builder.addAxisValueRecord(self, self.location) 2018 2019 def asFea(self, indent=""): 2020 res = "AxisValue {\n" 2021 for location in self.locations: 2022 res += location.asFea() 2023 2024 for nameRecord in self.names: 2025 res += nameRecord.asFea() 2026 res += "\n" 2027 2028 if self.flags: 2029 flags = ["OlderSiblingFontAttribute", "ElidableAxisValueName"] 2030 flagStrings = [] 2031 curr = 1 2032 for i in range(len(flags)): 2033 if self.flags & curr != 0: 2034 flagStrings.append(flags[i]) 2035 curr = curr << 1 2036 res += f"flag {' '.join(flagStrings)};\n" 2037 res += "};" 2038 return res 2039 2040 2041class AxisValueLocationStatement(Statement): 2042 """ 2043 A STAT table Axis Value Location 2044 2045 Args: 2046 tag (str): a 4 letter axis tag 2047 values (list): a list of ints and/or floats 2048 """ 2049 2050 def __init__(self, tag, values, location=None): 2051 Statement.__init__(self, location) 2052 self.tag = tag 2053 self.values = values 2054 2055 def asFea(self, res=""): 2056 res += f"location {self.tag} " 2057 res += f"{' '.join(str(i) for i in self.values)};\n" 2058 return res 2059 2060 2061class ConditionsetStatement(Statement): 2062 """ 2063 A variable layout conditionset 2064 2065 Args: 2066 name (str): the name of this conditionset 2067 conditions (dict): a dictionary mapping axis tags to a 2068 tuple of (min,max) userspace coordinates. 2069 """ 2070 2071 def __init__(self, name, conditions, location=None): 2072 Statement.__init__(self, location) 2073 self.name = name 2074 self.conditions = conditions 2075 2076 def build(self, builder): 2077 builder.add_conditionset(self.location, self.name, self.conditions) 2078 2079 def asFea(self, res="", indent=""): 2080 res += indent + f"conditionset {self.name} " + "{\n" 2081 for tag, (minvalue, maxvalue) in self.conditions.items(): 2082 res += indent + SHIFT + f"{tag} {minvalue} {maxvalue};\n" 2083 res += indent + "}" + f" {self.name};\n" 2084 return res 2085 2086 2087class VariationBlock(Block): 2088 """A variation feature block, applicable in a given set of conditions.""" 2089 2090 def __init__(self, name, conditionset, use_extension=False, location=None): 2091 Block.__init__(self, location) 2092 self.name, self.conditionset, self.use_extension = ( 2093 name, 2094 conditionset, 2095 use_extension, 2096 ) 2097 2098 def build(self, builder): 2099 """Call the ``start_feature`` callback on the builder object, visit 2100 all the statements in this feature, and then call ``end_feature``.""" 2101 builder.start_feature(self.location, self.name) 2102 if ( 2103 self.conditionset != "NULL" 2104 and self.conditionset not in builder.conditionsets_ 2105 ): 2106 raise FeatureLibError( 2107 f"variation block used undefined conditionset {self.conditionset}", 2108 self.location, 2109 ) 2110 2111 # language exclude_dflt statements modify builder.features_ 2112 # limit them to this block with temporary builder.features_ 2113 features = builder.features_ 2114 builder.features_ = {} 2115 Block.build(self, builder) 2116 for key, value in builder.features_.items(): 2117 items = builder.feature_variations_.setdefault(key, {}).setdefault( 2118 self.conditionset, [] 2119 ) 2120 items.extend(value) 2121 if key not in features: 2122 features[key] = [] # Ensure we make a feature record 2123 builder.features_ = features 2124 builder.end_feature() 2125 2126 def asFea(self, indent=""): 2127 res = indent + "variation %s " % self.name.strip() 2128 res += self.conditionset + " " 2129 if self.use_extension: 2130 res += "useExtension " 2131 res += "{\n" 2132 res += Block.asFea(self, indent=indent) 2133 res += indent + "} %s;\n" % self.name.strip() 2134 return res 2135