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
« 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
8SHIFT = " " * 4
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]
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)
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)
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
156class Element(object):
157 """A base class representing "something" in a feature file."""
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
165 def build(self, builder):
166 pass
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
174 def __str__(self):
175 return self.asFea()
178class Statement(Element):
179 pass
182class Expression(Element):
183 pass
186class Comment(Element):
187 """A comment in a feature file."""
189 def __init__(self, text, location=None):
190 super(Comment, self).__init__(location)
191 #: Text of the comment
192 self.text = text
194 def asFea(self, indent=""):
195 return self.text
198class NullGlyph(Expression):
199 """The NULL glyph, used in glyph deletion substitutions."""
201 def __init__(self, location=None):
202 Expression.__init__(self, location)
203 #: The name itself as a string
205 def glyphSet(self):
206 """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
207 return ()
209 def asFea(self, indent=""):
210 return "NULL"
213class GlyphName(Expression):
214 """A single glyph name, such as ``cedilla``."""
216 def __init__(self, glyph, location=None):
217 Expression.__init__(self, location)
218 #: The name itself as a string
219 self.glyph = glyph
221 def glyphSet(self):
222 """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
223 return (self.glyph,)
225 def asFea(self, indent=""):
226 return asFea(self.glyph)
229class GlyphClass(Expression):
230 """A glyph class, such as ``[acute cedilla grave]``."""
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
239 def glyphSet(self):
240 """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
241 return tuple(self.glyphs)
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)) + "]"
252 def extend(self, glyphs):
253 """Add a list of :class:`GlyphName` objects to the class."""
254 self.glyphs.extend(glyphs)
256 def append(self, glyph):
257 """Add a single :class:`GlyphName` object to the class."""
258 self.glyphs.append(glyph)
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)
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)
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)
291class GlyphClassName(Expression):
292 """A glyph class name, such as ``@FRENCH_MARKS``. This must be instantiated
293 with a :class:`GlyphClassDefinition` object."""
295 def __init__(self, glyphclass, location=None):
296 Expression.__init__(self, location)
297 assert isinstance(glyphclass, GlyphClassDefinition)
298 self.glyphclass = glyphclass
300 def glyphSet(self):
301 """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
302 return tuple(self.glyphclass.glyphSet())
304 def asFea(self, indent=""):
305 return "@" + self.glyphclass.name
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."""
312 def __init__(self, markClass, location=None):
313 Expression.__init__(self, location)
314 assert isinstance(markClass, MarkClass)
315 self.markClass = markClass
317 def glyphSet(self):
318 """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
319 return self.markClass.glyphSet()
321 def asFea(self, indent=""):
322 return "@" + self.markClass.name
325class AnonymousBlock(Statement):
326 """An anonymous data block."""
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
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
340class Block(Statement):
341 """A block of statements: feature, lookup, etc."""
343 def __init__(self, location=None):
344 Statement.__init__(self, location)
345 self.statements = [] #: Statements contained in the block
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)
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 )
363class FeatureFile(Block):
364 """The top-level element of the syntax tree, containing the whole feature
365 file in its ``statements`` attribute."""
367 def __init__(self):
368 Block.__init__(self, location=None)
369 self.markClasses = {} # name --> ast.MarkClass
371 def asFea(self, indent=""):
372 return "\n".join(s.asFea(indent=indent) for s in self.statements)
375class FeatureBlock(Block):
376 """A named feature block."""
378 def __init__(self, name, use_extension=False, location=None):
379 Block.__init__(self, location)
380 self.name, self.use_extension = name, use_extension
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()
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
407class NestedBlock(Block):
408 """A block inside another block, for example when found inside a
409 ``cvParameters`` block."""
411 def __init__(self, tag, block_name, location=None):
412 Block.__init__(self, location)
413 self.tag = tag
414 self.block_name = block_name
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)
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
428class LookupBlock(Block):
429 """A named lookup, containing ``statements``."""
431 def __init__(self, name, use_extension=False, location=None):
432 Block.__init__(self, location)
433 self.name, self.use_extension = name, use_extension
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()
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
451class TableBlock(Block):
452 """A ``table ... { }`` block."""
454 def __init__(self, name, location=None):
455 Block.__init__(self, location)
456 self.name = name
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
465class GlyphClassDefinition(Statement):
466 """Example: ``@UPPERCASE = [A-Z];``."""
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
473 def glyphSet(self):
474 """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
475 return tuple(self.glyphs.glyphSet())
477 def asFea(self, indent=""):
478 return "@" + self.name + " = " + self.glyphs.asFea() + ";"
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``."""
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
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)
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 )
511class MarkClass(object):
512 """One `or more` ``markClass`` statements for the same mark class.
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``::
519 markClass [acute grave] <anchor 350 800> @FRENCH_ACCENTS;
520 markClass [cedilla] <anchor 350 -200> @FRENCH_ACCENTS;
522 The ``MarkClass`` object is therefore just a container for a list of
523 :class:`MarkClassDefinition` statements.
524 """
526 def __init__(self, name):
527 self.name = name
528 self.definitions = []
529 self.glyphs = OrderedDict() # glyph --> ast.MarkClassDefinitions
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
547 def glyphSet(self):
548 """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
549 return tuple(self.glyphs.keys())
551 def asFea(self, indent=""):
552 res = "\n".join(d.asFea() for d in self.definitions)
553 return res
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`_ .
561 Example:
563 .. code:: python
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 ) )
573 mc.asFea()
574 # markClass [acute grave] <anchor 350 800> @FRENCH_ACCENTS;
575 # markClass [cedilla] <anchor 350 -200> @FRENCH_ACCENTS;
577 """
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
585 def glyphSet(self):
586 """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
587 return self.glyphs.glyphSet()
589 def asFea(self, indent=""):
590 return "markClass {} {} @{};".format(
591 self.glyphs.asFea(), self.anchor.asFea(), self.markClass.name
592 )
595class AlternateSubstStatement(Statement):
596 """A ``sub ... from ...`` statement.
598 ``prefix``, ``glyph``, ``suffix`` and ``replacement`` should be lists of
599 `glyph-containing objects`_. ``glyph`` should be a `one element list`."""
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
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)
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
632class Anchor(Expression):
633 """An ``Anchor`` element, used inside a ``pos`` rule.
635 If a ``name`` is given, this will be used in preference to the coordinates.
636 Other values should be integer.
637 """
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
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
669class AnchorDefinition(Statement):
670 """A named anchor definition. (2.e.viii). ``name`` should be a string."""
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
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
684class AttachStatement(Statement):
685 """A ``GDEF`` table ``Attach`` statement."""
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
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)
697 def asFea(self, indent=""):
698 return "Attach {} {};".format(
699 self.glyphs.asFea(), " ".join(str(c) for c in self.contourPoints)
700 )
703class ChainContextPosStatement(Statement):
704 r"""A chained contextual positioning statement.
706 ``prefix``, ``glyphs``, and ``suffix`` should be lists of
707 `glyph-containing objects`_ .
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."""
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]
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 )
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
761class ChainContextSubstStatement(Statement):
762 r"""A chained contextual substitution statement.
764 ``prefix``, ``glyphs``, and ``suffix`` should be lists of
765 `glyph-containing objects`_ .
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."""
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]
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 )
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
819class CursivePosStatement(Statement):
820 """A cursive positioning statement. Entry and exit anchors can either
821 be :class:`Anchor` objects or ``None``."""
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
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 )
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)
840class FeatureReferenceStatement(Statement):
841 """Example: ``feature salt;``"""
843 def __init__(self, featureName, location=None):
844 Statement.__init__(self, location)
845 self.location, self.featureName = (location, featureName)
847 def build(self, builder):
848 """Calls the builder object's ``add_feature_reference`` callback."""
849 builder.add_feature_reference(self.location, self.featureName)
851 def asFea(self, indent=""):
852 return "feature {};".format(self.featureName)
855class IgnorePosStatement(Statement):
856 """An ``ignore pos`` statement, containing `one or more` contexts to ignore.
858 ``chainContexts`` should be a list of ``(prefix, glyphs, suffix)`` tuples,
859 with each of ``prefix``, ``glyphs`` and ``suffix`` being
860 `glyph-containing objects`_ ."""
862 def __init__(self, chainContexts, location=None):
863 Statement.__init__(self, location)
864 self.chainContexts = chainContexts
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, [])
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) + ";"
891class IgnoreSubstStatement(Statement):
892 """An ``ignore sub`` statement, containing `one or more` contexts to ignore.
894 ``chainContexts`` should be a list of ``(prefix, glyphs, suffix)`` tuples,
895 with each of ``prefix``, ``glyphs`` and ``suffix`` being
896 `glyph-containing objects`_ ."""
898 def __init__(self, chainContexts, location=None):
899 Statement.__init__(self, location)
900 self.chainContexts = chainContexts
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, [])
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) + ";"
924class IncludeStatement(Statement):
925 """An ``include()`` statement."""
927 def __init__(self, filename, location=None):
928 super(IncludeStatement, self).__init__(location)
929 self.filename = filename #: String containing name of file to include
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 )
939 def asFea(self, indent=""):
940 return indent + "include(%s);" % self.filename
943class LanguageStatement(Statement):
944 """A ``language`` statement within a feature."""
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
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 )
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
972class LanguageSystemStatement(Statement):
973 """A top-level ``languagesystem`` statement."""
975 def __init__(self, script, language, location=None):
976 Statement.__init__(self, location)
977 self.script, self.language = (script, language)
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)
983 def asFea(self, indent=""):
984 return "languagesystem {} {};".format(self.script, self.language.strip())
987class FontRevisionStatement(Statement):
988 """A ``head`` table ``FontRevision`` statement. ``revision`` should be a
989 number, and will be formatted to three significant decimal places."""
991 def __init__(self, revision, location=None):
992 Statement.__init__(self, location)
993 self.revision = revision
995 def build(self, builder):
996 builder.set_font_revision(self.location, self.revision)
998 def asFea(self, indent=""):
999 return "FontRevision {:.3f};".format(self.revision)
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."""
1006 def __init__(self, glyphs, carets, location=None):
1007 Statement.__init__(self, location)
1008 self.glyphs, self.carets = (glyphs, carets)
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))
1015 def asFea(self, indent=""):
1016 return "LigatureCaretByIndex {} {};".format(
1017 self.glyphs.asFea(), " ".join(str(x) for x in self.carets)
1018 )
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."""
1025 def __init__(self, glyphs, carets, location=None):
1026 Statement.__init__(self, location)
1027 self.glyphs, self.carets = (glyphs, carets)
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))
1034 def asFea(self, indent=""):
1035 return "LigatureCaretByPos {} {};".format(
1036 self.glyphs.asFea(), " ".join(str(x) for x in self.carets)
1037 )
1040class LigatureSubstStatement(Statement):
1041 """A chained contextual substitution statement.
1043 ``prefix``, ``glyphs``, and ``suffix`` should be lists of
1044 `glyph-containing objects`_; ``replacement`` should be a single
1045 `glyph-containing object`_.
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."""
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
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 )
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
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."""
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
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)
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))
1120class LookupReferenceStatement(Statement):
1121 """Represents a ``lookup ...;`` statement to include a lookup in a feature.
1123 The ``lookup`` should be a :class:`LookupBlock` object."""
1125 def __init__(self, lookup, location=None):
1126 Statement.__init__(self, location)
1127 self.location, self.lookup = (location, lookup)
1129 def build(self, builder):
1130 """Calls the builder object's ``add_lookup_call`` callback."""
1131 builder.add_lookup_call(self.lookup.name)
1133 def asFea(self, indent=""):
1134 return "lookup {};".format(self.lookup.name)
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."""
1142 def __init__(self, base, marks, location=None):
1143 Statement.__init__(self, location)
1144 self.base, self.marks = base, marks
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)
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
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.
1165 Example::
1167 m1 = MarkClass("TOP_MARKS")
1168 m2 = MarkClass("BOTTOM_MARKS")
1169 # ... add definitions to mark classes...
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)
1179 mlp.asFea()
1180 # pos ligature lam_meem_jeem <anchor 625 1800> mark @TOP_MARKS
1181 # ligComponent <anchor 376 -378> mark @BOTTOM_MARKS;
1183 """
1185 def __init__(self, ligatures, marks, location=None):
1186 Statement.__init__(self, location)
1187 self.ligatures, self.marks = ligatures, marks
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)
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
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."""
1219 def __init__(self, baseMarks, marks, location=None):
1220 Statement.__init__(self, location)
1221 self.baseMarks, self.marks = baseMarks, marks
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)
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
1235class MultipleSubstStatement(Statement):
1236 """A multiple substitution statement.
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 """
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
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))
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 )
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
1305class PairPosStatement(Statement):
1306 """A pair positioning statement.
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 """
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
1329 def build(self, builder):
1330 """Calls a callback on the builder object:
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
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 )
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
1388class ReverseChainSingleSubstStatement(Statement):
1389 """A reverse chaining substitution statement. You don't see those every day.
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 """
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
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 )
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
1428class SingleSubstStatement(Statement):
1429 """A single substitution statement.
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 """
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
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 )
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
1474class ScriptStatement(Statement):
1475 """A ``script`` statement."""
1477 def __init__(self, script, location=None):
1478 Statement.__init__(self, location)
1479 self.script = script #: the script code
1481 def build(self, builder):
1482 """Calls the builder's ``set_script`` callback."""
1483 builder.set_script(self.location, self.script)
1485 def asFea(self, indent=""):
1486 return "script {};".format(self.script.strip())
1489class SinglePosStatement(Statement):
1490 """A single position statement. ``prefix`` and ``suffix`` should be
1491 lists of `glyph-containing objects`_.
1493 ``pos`` should be a one-element list containing a (`glyph-containing object`_,
1494 :class:`ValueRecord`) tuple."""
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
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)
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
1529class SubtableStatement(Statement):
1530 """Represents a subtable break."""
1532 def __init__(self, location=None):
1533 Statement.__init__(self, location)
1535 def build(self, builder):
1536 """Calls the builder objects's ``add_subtable_break`` callback."""
1537 builder.add_subtable_break(self.location)
1539 def asFea(self, indent=""):
1540 return "subtable;"
1543class ValueRecord(Expression):
1544 """Represents a value record."""
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
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 )
1576 def __ne__(self, other):
1577 return not self.__eq__(other)
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 )
1591 def asFea(self, indent=""):
1592 if not self:
1593 return "<NULL>"
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
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)
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
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)
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 )
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 )
1650 __nonzero__ = __bool__
1653class ValueRecordDefinition(Statement):
1654 """Represents a named value record definition."""
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
1661 def asFea(self, indent=""):
1662 return "valueRecordDef {} {};".format(self.value.asFea(), self.name)
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)
1674class NameRecord(Statement):
1675 """Represents a name record. (`Section 9.e. <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.e>`_)"""
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
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 )
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
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)
1723class FeatureNameStatement(NameRecord):
1724 """Represents a ``sizemenuname`` or ``name`` statement."""
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)
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)
1742class STATNameStatement(NameRecord):
1743 """Represents a STAT table ``name`` statement."""
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)
1752class SizeParameters(Statement):
1753 """A ``parameters`` statement."""
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
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 )
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 + ";"
1779class CVParametersNameStatement(NameRecord):
1780 """Represent a name statement inside a ``cvParameters`` block."""
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
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)
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)
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 """
1814 def __init__(self, character, tag, location=None):
1815 Statement.__init__(self, location)
1816 self.character = character
1817 self.tag = tag
1819 def build(self, builder):
1820 """Calls the builder object's ``add_cv_character`` callback."""
1821 builder.add_cv_character(self.character, self.tag)
1823 def asFea(self, indent=""):
1824 return "Character {:#x};".format(self.character)
1827class BaseAxis(Statement):
1828 """An axis definition, being either a ``VertAxis.BaseTagList/BaseScriptList``
1829 pair or a ``HorizAxis.BaseTagList/BaseScriptList`` pair."""
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
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)
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 )
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."""
1857 def __init__(self, key, value, location=None):
1858 Statement.__init__(self, location)
1859 self.key = key
1860 self.value = value
1862 def build(self, builder):
1863 """Calls the builder object's ``add_os2_field`` callback."""
1864 builder.add_os2_field(self.key, self.value)
1866 def asFea(self, indent=""):
1867 def intarr2str(x):
1868 return " ".join(map(str, x))
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
1896class HheaField(Statement):
1897 """An entry in the ``hhea`` table."""
1899 def __init__(self, key, value, location=None):
1900 Statement.__init__(self, location)
1901 self.key = key
1902 self.value = value
1904 def build(self, builder):
1905 """Calls the builder object's ``add_hhea_field`` callback."""
1906 builder.add_hhea_field(self.key, self.value)
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)
1914class VheaField(Statement):
1915 """An entry in the ``vhea`` table."""
1917 def __init__(self, key, value, location=None):
1918 Statement.__init__(self, location)
1919 self.key = key
1920 self.value = value
1922 def build(self, builder):
1923 """Calls the builder object's ``add_vhea_field`` callback."""
1924 builder.add_vhea_field(self.key, self.value)
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)
1932class STATDesignAxisStatement(Statement):
1933 """A STAT table Design Axis
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 """
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
1948 def build(self, builder):
1949 builder.addDesignAxis(self, self.location)
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
1959class ElidedFallbackName(Statement):
1960 """STAT table ElidedFallbackName
1962 Args:
1963 names: a list of :class:`STATNameStatement` objects
1964 """
1966 def __init__(self, names, location=None):
1967 Statement.__init__(self, location)
1968 self.names = names
1969 self.location = location
1971 def build(self, builder):
1972 builder.setElidedFallbackName(self.names, self.location)
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
1982class ElidedFallbackNameID(Statement):
1983 """STAT table ElidedFallbackNameID
1985 Args:
1986 value: an int pointing to an existing name table name ID
1987 """
1989 def __init__(self, value, location=None):
1990 Statement.__init__(self, location)
1991 self.value = value
1992 self.location = location
1994 def build(self, builder):
1995 builder.setElidedFallbackName(self.value, self.location)
1997 def asFea(self, indent=""):
1998 return f"ElidedFallbackNameID {self.value};"
2001class STATAxisValueStatement(Statement):
2002 """A STAT table Axis Value Record
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 """
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
2016 def build(self, builder):
2017 builder.addAxisValueRecord(self, self.location)
2019 def asFea(self, indent=""):
2020 res = "AxisValue {\n"
2021 for location in self.locations:
2022 res += location.asFea()
2024 for nameRecord in self.names:
2025 res += nameRecord.asFea()
2026 res += "\n"
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
2041class AxisValueLocationStatement(Statement):
2042 """
2043 A STAT table Axis Value Location
2045 Args:
2046 tag (str): a 4 letter axis tag
2047 values (list): a list of ints and/or floats
2048 """
2050 def __init__(self, tag, values, location=None):
2051 Statement.__init__(self, location)
2052 self.tag = tag
2053 self.values = values
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
2061class ConditionsetStatement(Statement):
2062 """
2063 A variable layout conditionset
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 """
2071 def __init__(self, name, conditions, location=None):
2072 Statement.__init__(self, location)
2073 self.name = name
2074 self.conditions = conditions
2076 def build(self, builder):
2077 builder.add_conditionset(self.location, self.name, self.conditions)
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
2087class VariationBlock(Block):
2088 """A variation feature block, applicable in a given set of conditions."""
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 )
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 )
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()
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