Coverage for /usr/lib/python3/dist-packages/fontTools/feaLib/ast.py: 26%

1060 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

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