2144 lines
		
	
	
		
			72 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			2144 lines
		
	
	
		
			72 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| import weakref
 | |
| from fontTools.feaLib.error import FeatureLibError
 | |
| from fontTools.feaLib.location import FeatureLibLocation
 | |
| from fontTools.misc.encodingTools import getEncoding
 | |
| from fontTools.misc.textTools import byteord, tobytes
 | |
| from collections import OrderedDict
 | |
| import itertools
 | |
| 
 | |
| SHIFT = " " * 4
 | |
| 
 | |
| __all__ = [
 | |
|     "Element",
 | |
|     "FeatureFile",
 | |
|     "Comment",
 | |
|     "GlyphName",
 | |
|     "GlyphClass",
 | |
|     "GlyphClassName",
 | |
|     "MarkClassName",
 | |
|     "AnonymousBlock",
 | |
|     "Block",
 | |
|     "FeatureBlock",
 | |
|     "NestedBlock",
 | |
|     "LookupBlock",
 | |
|     "GlyphClassDefinition",
 | |
|     "GlyphClassDefStatement",
 | |
|     "MarkClass",
 | |
|     "MarkClassDefinition",
 | |
|     "AlternateSubstStatement",
 | |
|     "Anchor",
 | |
|     "AnchorDefinition",
 | |
|     "AttachStatement",
 | |
|     "AxisValueLocationStatement",
 | |
|     "BaseAxis",
 | |
|     "CVParametersNameStatement",
 | |
|     "ChainContextPosStatement",
 | |
|     "ChainContextSubstStatement",
 | |
|     "CharacterStatement",
 | |
|     "ConditionsetStatement",
 | |
|     "CursivePosStatement",
 | |
|     "ElidedFallbackName",
 | |
|     "ElidedFallbackNameID",
 | |
|     "Expression",
 | |
|     "FeatureNameStatement",
 | |
|     "FeatureReferenceStatement",
 | |
|     "FontRevisionStatement",
 | |
|     "HheaField",
 | |
|     "IgnorePosStatement",
 | |
|     "IgnoreSubstStatement",
 | |
|     "IncludeStatement",
 | |
|     "LanguageStatement",
 | |
|     "LanguageSystemStatement",
 | |
|     "LigatureCaretByIndexStatement",
 | |
|     "LigatureCaretByPosStatement",
 | |
|     "LigatureSubstStatement",
 | |
|     "LookupFlagStatement",
 | |
|     "LookupReferenceStatement",
 | |
|     "MarkBasePosStatement",
 | |
|     "MarkLigPosStatement",
 | |
|     "MarkMarkPosStatement",
 | |
|     "MultipleSubstStatement",
 | |
|     "NameRecord",
 | |
|     "OS2Field",
 | |
|     "PairPosStatement",
 | |
|     "ReverseChainSingleSubstStatement",
 | |
|     "ScriptStatement",
 | |
|     "SinglePosStatement",
 | |
|     "SingleSubstStatement",
 | |
|     "SizeParameters",
 | |
|     "Statement",
 | |
|     "STATAxisValueStatement",
 | |
|     "STATDesignAxisStatement",
 | |
|     "STATNameStatement",
 | |
|     "SubtableStatement",
 | |
|     "TableBlock",
 | |
|     "ValueRecord",
 | |
|     "ValueRecordDefinition",
 | |
|     "VheaField",
 | |
| ]
 | |
| 
 | |
| 
 | |
| def deviceToString(device):
 | |
|     if device is None:
 | |
|         return "<device NULL>"
 | |
|     else:
 | |
|         return "<device %s>" % ", ".join("%d %d" % t for t in device)
 | |
| 
 | |
| 
 | |
| fea_keywords = set(
 | |
|     [
 | |
|         "anchor",
 | |
|         "anchordef",
 | |
|         "anon",
 | |
|         "anonymous",
 | |
|         "by",
 | |
|         "contour",
 | |
|         "cursive",
 | |
|         "device",
 | |
|         "enum",
 | |
|         "enumerate",
 | |
|         "excludedflt",
 | |
|         "exclude_dflt",
 | |
|         "feature",
 | |
|         "from",
 | |
|         "ignore",
 | |
|         "ignorebaseglyphs",
 | |
|         "ignoreligatures",
 | |
|         "ignoremarks",
 | |
|         "include",
 | |
|         "includedflt",
 | |
|         "include_dflt",
 | |
|         "language",
 | |
|         "languagesystem",
 | |
|         "lookup",
 | |
|         "lookupflag",
 | |
|         "mark",
 | |
|         "markattachmenttype",
 | |
|         "markclass",
 | |
|         "nameid",
 | |
|         "null",
 | |
|         "parameters",
 | |
|         "pos",
 | |
|         "position",
 | |
|         "required",
 | |
|         "righttoleft",
 | |
|         "reversesub",
 | |
|         "rsub",
 | |
|         "script",
 | |
|         "sub",
 | |
|         "substitute",
 | |
|         "subtable",
 | |
|         "table",
 | |
|         "usemarkfilteringset",
 | |
|         "useextension",
 | |
|         "valuerecorddef",
 | |
|         "base",
 | |
|         "gdef",
 | |
|         "head",
 | |
|         "hhea",
 | |
|         "name",
 | |
|         "vhea",
 | |
|         "vmtx",
 | |
|     ]
 | |
| )
 | |
| 
 | |
| 
 | |
| def asFea(g):
 | |
|     if hasattr(g, "asFea"):
 | |
|         return g.asFea()
 | |
|     elif isinstance(g, tuple) and len(g) == 2:
 | |
|         return asFea(g[0]) + " - " + asFea(g[1])  # a range
 | |
|     elif g.lower() in fea_keywords:
 | |
|         return "\\" + g
 | |
|     else:
 | |
|         return g
 | |
| 
 | |
| 
 | |
| class Element(object):
 | |
|     """A base class representing "something" in a feature file."""
 | |
| 
 | |
|     def __init__(self, location=None):
 | |
|         #: location of this element as a `FeatureLibLocation` object.
 | |
|         if location and not isinstance(location, FeatureLibLocation):
 | |
|             location = FeatureLibLocation(*location)
 | |
|         self.location = location
 | |
| 
 | |
|     def build(self, builder):
 | |
|         pass
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         """Returns this element as a string of feature code. For block-type
 | |
|         elements (such as :class:`FeatureBlock`), the `indent` string is
 | |
|         added to the start of each line in the output."""
 | |
|         raise NotImplementedError
 | |
| 
 | |
|     def __str__(self):
 | |
|         return self.asFea()
 | |
| 
 | |
| 
 | |
| class Statement(Element):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| class Expression(Element):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| class Comment(Element):
 | |
|     """A comment in a feature file."""
 | |
| 
 | |
|     def __init__(self, text, location=None):
 | |
|         super(Comment, self).__init__(location)
 | |
|         #: Text of the comment
 | |
|         self.text = text
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         return self.text
 | |
| 
 | |
| 
 | |
| class NullGlyph(Expression):
 | |
|     """The NULL glyph, used in glyph deletion substitutions."""
 | |
| 
 | |
|     def __init__(self, location=None):
 | |
|         Expression.__init__(self, location)
 | |
|         #: The name itself as a string
 | |
| 
 | |
|     def glyphSet(self):
 | |
|         """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
 | |
|         return ()
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         return "NULL"
 | |
| 
 | |
| 
 | |
| class GlyphName(Expression):
 | |
|     """A single glyph name, such as ``cedilla``."""
 | |
| 
 | |
|     def __init__(self, glyph, location=None):
 | |
|         Expression.__init__(self, location)
 | |
|         #: The name itself as a string
 | |
|         self.glyph = glyph
 | |
| 
 | |
|     def glyphSet(self):
 | |
|         """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
 | |
|         return (self.glyph,)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         return asFea(self.glyph)
 | |
| 
 | |
| 
 | |
| class GlyphClass(Expression):
 | |
|     """A glyph class, such as ``[acute cedilla grave]``."""
 | |
| 
 | |
|     def __init__(self, glyphs=None, location=None):
 | |
|         Expression.__init__(self, location)
 | |
|         #: The list of glyphs in this class, as :class:`GlyphName` objects.
 | |
|         self.glyphs = glyphs if glyphs is not None else []
 | |
|         self.original = []
 | |
|         self.curr = 0
 | |
| 
 | |
|     def glyphSet(self):
 | |
|         """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
 | |
|         return tuple(self.glyphs)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         if len(self.original):
 | |
|             if self.curr < len(self.glyphs):
 | |
|                 self.original.extend(self.glyphs[self.curr :])
 | |
|                 self.curr = len(self.glyphs)
 | |
|             return "[" + " ".join(map(asFea, self.original)) + "]"
 | |
|         else:
 | |
|             return "[" + " ".join(map(asFea, self.glyphs)) + "]"
 | |
| 
 | |
|     def extend(self, glyphs):
 | |
|         """Add a list of :class:`GlyphName` objects to the class."""
 | |
|         self.glyphs.extend(glyphs)
 | |
| 
 | |
|     def append(self, glyph):
 | |
|         """Add a single :class:`GlyphName` object to the class."""
 | |
|         self.glyphs.append(glyph)
 | |
| 
 | |
|     def add_range(self, start, end, glyphs):
 | |
|         """Add a range (e.g. ``A-Z``) to the class. ``start`` and ``end``
 | |
|         are either :class:`GlyphName` objects or strings representing the
 | |
|         start and end glyphs in the class, and ``glyphs`` is the full list of
 | |
|         :class:`GlyphName` objects in the range."""
 | |
|         if self.curr < len(self.glyphs):
 | |
|             self.original.extend(self.glyphs[self.curr :])
 | |
|         self.original.append((start, end))
 | |
|         self.glyphs.extend(glyphs)
 | |
|         self.curr = len(self.glyphs)
 | |
| 
 | |
|     def add_cid_range(self, start, end, glyphs):
 | |
|         """Add a range to the class by glyph ID. ``start`` and ``end`` are the
 | |
|         initial and final IDs, and ``glyphs`` is the full list of
 | |
|         :class:`GlyphName` objects in the range."""
 | |
|         if self.curr < len(self.glyphs):
 | |
|             self.original.extend(self.glyphs[self.curr :])
 | |
|         self.original.append(("\\{}".format(start), "\\{}".format(end)))
 | |
|         self.glyphs.extend(glyphs)
 | |
|         self.curr = len(self.glyphs)
 | |
| 
 | |
|     def add_class(self, gc):
 | |
|         """Add glyphs from the given :class:`GlyphClassName` object to the
 | |
|         class."""
 | |
|         if self.curr < len(self.glyphs):
 | |
|             self.original.extend(self.glyphs[self.curr :])
 | |
|         self.original.append(gc)
 | |
|         self.glyphs.extend(gc.glyphSet())
 | |
|         self.curr = len(self.glyphs)
 | |
| 
 | |
| 
 | |
| class GlyphClassName(Expression):
 | |
|     """A glyph class name, such as ``@FRENCH_MARKS``. This must be instantiated
 | |
|     with a :class:`GlyphClassDefinition` object."""
 | |
| 
 | |
|     def __init__(self, glyphclass, location=None):
 | |
|         Expression.__init__(self, location)
 | |
|         assert isinstance(glyphclass, GlyphClassDefinition)
 | |
|         self.glyphclass = glyphclass
 | |
| 
 | |
|     def glyphSet(self):
 | |
|         """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
 | |
|         return tuple(self.glyphclass.glyphSet())
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         return "@" + self.glyphclass.name
 | |
| 
 | |
| 
 | |
| class MarkClassName(Expression):
 | |
|     """A mark class name, such as ``@FRENCH_MARKS`` defined with ``markClass``.
 | |
|     This must be instantiated with a :class:`MarkClass` object."""
 | |
| 
 | |
|     def __init__(self, markClass, location=None):
 | |
|         Expression.__init__(self, location)
 | |
|         assert isinstance(markClass, MarkClass)
 | |
|         self.markClass = markClass
 | |
| 
 | |
|     def glyphSet(self):
 | |
|         """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
 | |
|         return self.markClass.glyphSet()
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         return "@" + self.markClass.name
 | |
| 
 | |
| 
 | |
| class AnonymousBlock(Statement):
 | |
|     """An anonymous data block."""
 | |
| 
 | |
|     def __init__(self, tag, content, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.tag = tag  #: string containing the block's "tag"
 | |
|         self.content = content  #: block data as string
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = "anon {} {{\n".format(self.tag)
 | |
|         res += self.content
 | |
|         res += "}} {};\n\n".format(self.tag)
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class Block(Statement):
 | |
|     """A block of statements: feature, lookup, etc."""
 | |
| 
 | |
|     def __init__(self, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.statements = []  #: Statements contained in the block
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """When handed a 'builder' object of comparable interface to
 | |
|         :class:`fontTools.feaLib.builder`, walks the statements in this
 | |
|         block, calling the builder callbacks."""
 | |
|         for s in self.statements:
 | |
|             s.build(builder)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         indent += SHIFT
 | |
|         return (
 | |
|             indent
 | |
|             + ("\n" + indent).join([s.asFea(indent=indent) for s in self.statements])
 | |
|             + "\n"
 | |
|         )
 | |
| 
 | |
| 
 | |
| class FeatureFile(Block):
 | |
|     """The top-level element of the syntax tree, containing the whole feature
 | |
|     file in its ``statements`` attribute."""
 | |
| 
 | |
|     def __init__(self):
 | |
|         Block.__init__(self, location=None)
 | |
|         self.markClasses = {}  # name --> ast.MarkClass
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         return "\n".join(s.asFea(indent=indent) for s in self.statements)
 | |
| 
 | |
| 
 | |
| class FeatureBlock(Block):
 | |
|     """A named feature block."""
 | |
| 
 | |
|     def __init__(self, name, use_extension=False, location=None):
 | |
|         Block.__init__(self, location)
 | |
|         self.name, self.use_extension = name, use_extension
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Call the ``start_feature`` callback on the builder object, visit
 | |
|         all the statements in this feature, and then call ``end_feature``."""
 | |
|         builder.start_feature(self.location, self.name, self.use_extension)
 | |
|         # language exclude_dflt statements modify builder.features_
 | |
|         # limit them to this block with temporary builder.features_
 | |
|         features = builder.features_
 | |
|         builder.features_ = {}
 | |
|         Block.build(self, builder)
 | |
|         for key, value in builder.features_.items():
 | |
|             features.setdefault(key, []).extend(value)
 | |
|         builder.features_ = features
 | |
|         builder.end_feature()
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = indent + "feature %s " % self.name.strip()
 | |
|         if self.use_extension:
 | |
|             res += "useExtension "
 | |
|         res += "{\n"
 | |
|         res += Block.asFea(self, indent=indent)
 | |
|         res += indent + "} %s;\n" % self.name.strip()
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class NestedBlock(Block):
 | |
|     """A block inside another block, for example when found inside a
 | |
|     ``cvParameters`` block."""
 | |
| 
 | |
|     def __init__(self, tag, block_name, location=None):
 | |
|         Block.__init__(self, location)
 | |
|         self.tag = tag
 | |
|         self.block_name = block_name
 | |
| 
 | |
|     def build(self, builder):
 | |
|         Block.build(self, builder)
 | |
|         if self.block_name == "ParamUILabelNameID":
 | |
|             builder.add_to_cv_num_named_params(self.tag)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = "{}{} {{\n".format(indent, self.block_name)
 | |
|         res += Block.asFea(self, indent=indent)
 | |
|         res += "{}}};\n".format(indent)
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class LookupBlock(Block):
 | |
|     """A named lookup, containing ``statements``."""
 | |
| 
 | |
|     def __init__(self, name, use_extension=False, location=None):
 | |
|         Block.__init__(self, location)
 | |
|         self.name, self.use_extension = name, use_extension
 | |
| 
 | |
|     def build(self, builder):
 | |
|         builder.start_lookup_block(self.location, self.name, self.use_extension)
 | |
|         Block.build(self, builder)
 | |
|         builder.end_lookup_block()
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = "lookup {} ".format(self.name)
 | |
|         if self.use_extension:
 | |
|             res += "useExtension "
 | |
|         res += "{\n"
 | |
|         res += Block.asFea(self, indent=indent)
 | |
|         res += "{}}} {};\n".format(indent, self.name)
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class TableBlock(Block):
 | |
|     """A ``table ... { }`` block."""
 | |
| 
 | |
|     def __init__(self, name, location=None):
 | |
|         Block.__init__(self, location)
 | |
|         self.name = name
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = "table {} {{\n".format(self.name.strip())
 | |
|         res += super(TableBlock, self).asFea(indent=indent)
 | |
|         res += "}} {};\n".format(self.name.strip())
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class GlyphClassDefinition(Statement):
 | |
|     """Example: ``@UPPERCASE = [A-Z];``."""
 | |
| 
 | |
|     def __init__(self, name, glyphs, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.name = name  #: class name as a string, without initial ``@``
 | |
|         self.glyphs = glyphs  #: a :class:`GlyphClass` object
 | |
| 
 | |
|     def glyphSet(self):
 | |
|         """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
 | |
|         return tuple(self.glyphs.glyphSet())
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         return "@" + self.name + " = " + self.glyphs.asFea() + ";"
 | |
| 
 | |
| 
 | |
| class GlyphClassDefStatement(Statement):
 | |
|     """Example: ``GlyphClassDef @UPPERCASE, [B], [C], [D];``. The parameters
 | |
|     must be either :class:`GlyphClass` or :class:`GlyphClassName` objects, or
 | |
|     ``None``."""
 | |
| 
 | |
|     def __init__(
 | |
|         self, baseGlyphs, markGlyphs, ligatureGlyphs, componentGlyphs, location=None
 | |
|     ):
 | |
|         Statement.__init__(self, location)
 | |
|         self.baseGlyphs, self.markGlyphs = (baseGlyphs, markGlyphs)
 | |
|         self.ligatureGlyphs = ligatureGlyphs
 | |
|         self.componentGlyphs = componentGlyphs
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder's ``add_glyphClassDef`` callback."""
 | |
|         base = self.baseGlyphs.glyphSet() if self.baseGlyphs else tuple()
 | |
|         liga = self.ligatureGlyphs.glyphSet() if self.ligatureGlyphs else tuple()
 | |
|         mark = self.markGlyphs.glyphSet() if self.markGlyphs else tuple()
 | |
|         comp = self.componentGlyphs.glyphSet() if self.componentGlyphs else tuple()
 | |
|         builder.add_glyphClassDef(self.location, base, liga, mark, comp)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         return "GlyphClassDef {}, {}, {}, {};".format(
 | |
|             self.baseGlyphs.asFea() if self.baseGlyphs else "",
 | |
|             self.ligatureGlyphs.asFea() if self.ligatureGlyphs else "",
 | |
|             self.markGlyphs.asFea() if self.markGlyphs else "",
 | |
|             self.componentGlyphs.asFea() if self.componentGlyphs else "",
 | |
|         )
 | |
| 
 | |
| 
 | |
| class MarkClass(object):
 | |
|     """One `or more` ``markClass`` statements for the same mark class.
 | |
| 
 | |
|     While glyph classes can be defined only once, the feature file format
 | |
|     allows expanding mark classes with multiple definitions, each using
 | |
|     different glyphs and anchors. The following are two ``MarkClassDefinitions``
 | |
|     for the same ``MarkClass``::
 | |
| 
 | |
|         markClass [acute grave] <anchor 350 800> @FRENCH_ACCENTS;
 | |
|         markClass [cedilla] <anchor 350 -200> @FRENCH_ACCENTS;
 | |
| 
 | |
|     The ``MarkClass`` object is therefore just a container for a list of
 | |
|     :class:`MarkClassDefinition` statements.
 | |
|     """
 | |
| 
 | |
|     def __init__(self, name):
 | |
|         self.name = name
 | |
|         self.definitions = []
 | |
|         self.glyphs = OrderedDict()  # glyph --> ast.MarkClassDefinitions
 | |
| 
 | |
|     def addDefinition(self, definition):
 | |
|         """Add a :class:`MarkClassDefinition` statement to this mark class."""
 | |
|         assert isinstance(definition, MarkClassDefinition)
 | |
|         self.definitions.append(weakref.proxy(definition))
 | |
|         for glyph in definition.glyphSet():
 | |
|             if glyph in self.glyphs:
 | |
|                 otherLoc = self.glyphs[glyph].location
 | |
|                 if otherLoc is None:
 | |
|                     end = ""
 | |
|                 else:
 | |
|                     end = f" at {otherLoc}"
 | |
|                 raise FeatureLibError(
 | |
|                     "Glyph %s already defined%s" % (glyph, end), definition.location
 | |
|                 )
 | |
|             self.glyphs[glyph] = definition
 | |
| 
 | |
|     def glyphSet(self):
 | |
|         """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
 | |
|         return tuple(self.glyphs.keys())
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = "\n".join(d.asFea() for d in self.definitions)
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class MarkClassDefinition(Statement):
 | |
|     """A single ``markClass`` statement. The ``markClass`` should be a
 | |
|     :class:`MarkClass` object, the ``anchor`` an :class:`Anchor` object,
 | |
|     and the ``glyphs`` parameter should be a `glyph-containing object`_ .
 | |
| 
 | |
|     Example:
 | |
| 
 | |
|         .. code:: python
 | |
| 
 | |
|             mc = MarkClass("FRENCH_ACCENTS")
 | |
|             mc.addDefinition( MarkClassDefinition(mc, Anchor(350, 800),
 | |
|                 GlyphClass([ GlyphName("acute"), GlyphName("grave") ])
 | |
|             ) )
 | |
|             mc.addDefinition( MarkClassDefinition(mc, Anchor(350, -200),
 | |
|                 GlyphClass([ GlyphName("cedilla") ])
 | |
|             ) )
 | |
| 
 | |
|             mc.asFea()
 | |
|             # markClass [acute grave] <anchor 350 800> @FRENCH_ACCENTS;
 | |
|             # markClass [cedilla] <anchor 350 -200> @FRENCH_ACCENTS;
 | |
| 
 | |
|     """
 | |
| 
 | |
|     def __init__(self, markClass, anchor, glyphs, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         assert isinstance(markClass, MarkClass)
 | |
|         assert isinstance(anchor, Anchor) and isinstance(glyphs, Expression)
 | |
|         self.markClass, self.anchor, self.glyphs = markClass, anchor, glyphs
 | |
| 
 | |
|     def glyphSet(self):
 | |
|         """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
 | |
|         return self.glyphs.glyphSet()
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         return "markClass {} {} @{};".format(
 | |
|             self.glyphs.asFea(), self.anchor.asFea(), self.markClass.name
 | |
|         )
 | |
| 
 | |
| 
 | |
| class AlternateSubstStatement(Statement):
 | |
|     """A ``sub ... from ...`` statement.
 | |
| 
 | |
|     ``glyph`` and ``replacement`` should be `glyph-containing objects`_.
 | |
|     ``prefix`` and ``suffix`` should be lists of `glyph-containing objects`_."""
 | |
| 
 | |
|     def __init__(self, prefix, glyph, suffix, replacement, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.prefix, self.glyph, self.suffix = (prefix, glyph, suffix)
 | |
|         self.replacement = replacement
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder's ``add_alternate_subst`` callback."""
 | |
|         glyph = self.glyph.glyphSet()
 | |
|         assert len(glyph) == 1, glyph
 | |
|         glyph = list(glyph)[0]
 | |
|         prefix = [p.glyphSet() for p in self.prefix]
 | |
|         suffix = [s.glyphSet() for s in self.suffix]
 | |
|         replacement = self.replacement.glyphSet()
 | |
|         builder.add_alternate_subst(self.location, prefix, glyph, suffix, replacement)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = "sub "
 | |
|         if len(self.prefix) or len(self.suffix):
 | |
|             if len(self.prefix):
 | |
|                 res += " ".join(map(asFea, self.prefix)) + " "
 | |
|             res += asFea(self.glyph) + "'"  # even though we really only use 1
 | |
|             if len(self.suffix):
 | |
|                 res += " " + " ".join(map(asFea, self.suffix))
 | |
|         else:
 | |
|             res += asFea(self.glyph)
 | |
|         res += " from "
 | |
|         res += asFea(self.replacement)
 | |
|         res += ";"
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class Anchor(Expression):
 | |
|     """An ``Anchor`` element, used inside a ``pos`` rule.
 | |
| 
 | |
|     If a ``name`` is given, this will be used in preference to the coordinates.
 | |
|     Other values should be integer.
 | |
|     """
 | |
| 
 | |
|     def __init__(
 | |
|         self,
 | |
|         x,
 | |
|         y,
 | |
|         name=None,
 | |
|         contourpoint=None,
 | |
|         xDeviceTable=None,
 | |
|         yDeviceTable=None,
 | |
|         location=None,
 | |
|     ):
 | |
|         Expression.__init__(self, location)
 | |
|         self.name = name
 | |
|         self.x, self.y, self.contourpoint = x, y, contourpoint
 | |
|         self.xDeviceTable, self.yDeviceTable = xDeviceTable, yDeviceTable
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         if self.name is not None:
 | |
|             return "<anchor {}>".format(self.name)
 | |
|         res = "<anchor {} {}".format(self.x, self.y)
 | |
|         if self.contourpoint:
 | |
|             res += " contourpoint {}".format(self.contourpoint)
 | |
|         if self.xDeviceTable or self.yDeviceTable:
 | |
|             res += " "
 | |
|             res += deviceToString(self.xDeviceTable)
 | |
|             res += " "
 | |
|             res += deviceToString(self.yDeviceTable)
 | |
|         res += ">"
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class AnchorDefinition(Statement):
 | |
|     """A named anchor definition. (2.e.viii). ``name`` should be a string."""
 | |
| 
 | |
|     def __init__(self, name, x, y, contourpoint=None, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.name, self.x, self.y, self.contourpoint = name, x, y, contourpoint
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = "anchorDef {} {}".format(self.x, self.y)
 | |
|         if self.contourpoint:
 | |
|             res += " contourpoint {}".format(self.contourpoint)
 | |
|         res += " {};".format(self.name)
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class AttachStatement(Statement):
 | |
|     """A ``GDEF`` table ``Attach`` statement."""
 | |
| 
 | |
|     def __init__(self, glyphs, contourPoints, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.glyphs = glyphs  #: A `glyph-containing object`_
 | |
|         self.contourPoints = contourPoints  #: A list of integer contour points
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder's ``add_attach_points`` callback."""
 | |
|         glyphs = self.glyphs.glyphSet()
 | |
|         builder.add_attach_points(self.location, glyphs, self.contourPoints)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         return "Attach {} {};".format(
 | |
|             self.glyphs.asFea(), " ".join(str(c) for c in self.contourPoints)
 | |
|         )
 | |
| 
 | |
| 
 | |
| class ChainContextPosStatement(Statement):
 | |
|     r"""A chained contextual positioning statement.
 | |
| 
 | |
|     ``prefix``, ``glyphs``, and ``suffix`` should be lists of
 | |
|     `glyph-containing objects`_ .
 | |
| 
 | |
|     ``lookups`` should be a list of elements representing what lookups
 | |
|     to apply at each glyph position. Each element should be a
 | |
|     :class:`LookupBlock` to apply a single chaining lookup at the given
 | |
|     position, a list of :class:`LookupBlock`\ s to apply multiple
 | |
|     lookups, or ``None`` to apply no lookup. The length of the outer
 | |
|     list should equal the length of ``glyphs``; the inner lists can be
 | |
|     of variable length."""
 | |
| 
 | |
|     def __init__(self, prefix, glyphs, suffix, lookups, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix
 | |
|         self.lookups = list(lookups)
 | |
|         for i, lookup in enumerate(lookups):
 | |
|             if lookup:
 | |
|                 try:
 | |
|                     iter(lookup)
 | |
|                 except TypeError:
 | |
|                     self.lookups[i] = [lookup]
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder's ``add_chain_context_pos`` callback."""
 | |
|         prefix = [p.glyphSet() for p in self.prefix]
 | |
|         glyphs = [g.glyphSet() for g in self.glyphs]
 | |
|         suffix = [s.glyphSet() for s in self.suffix]
 | |
|         builder.add_chain_context_pos(
 | |
|             self.location, prefix, glyphs, suffix, self.lookups
 | |
|         )
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = "pos "
 | |
|         if (
 | |
|             len(self.prefix)
 | |
|             or len(self.suffix)
 | |
|             or any([x is not None for x in self.lookups])
 | |
|         ):
 | |
|             if len(self.prefix):
 | |
|                 res += " ".join(g.asFea() for g in self.prefix) + " "
 | |
|             for i, g in enumerate(self.glyphs):
 | |
|                 res += g.asFea() + "'"
 | |
|                 if self.lookups[i]:
 | |
|                     for lu in self.lookups[i]:
 | |
|                         res += " lookup " + lu.name
 | |
|                 if i < len(self.glyphs) - 1:
 | |
|                     res += " "
 | |
|             if len(self.suffix):
 | |
|                 res += " " + " ".join(map(asFea, self.suffix))
 | |
|         else:
 | |
|             res += " ".join(map(asFea, self.glyphs))
 | |
|         res += ";"
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class ChainContextSubstStatement(Statement):
 | |
|     r"""A chained contextual substitution statement.
 | |
| 
 | |
|     ``prefix``, ``glyphs``, and ``suffix`` should be lists of
 | |
|     `glyph-containing objects`_ .
 | |
| 
 | |
|     ``lookups`` should be a list of elements representing what lookups
 | |
|     to apply at each glyph position. Each element should be a
 | |
|     :class:`LookupBlock` to apply a single chaining lookup at the given
 | |
|     position, a list of :class:`LookupBlock`\ s to apply multiple
 | |
|     lookups, or ``None`` to apply no lookup. The length of the outer
 | |
|     list should equal the length of ``glyphs``; the inner lists can be
 | |
|     of variable length."""
 | |
| 
 | |
|     def __init__(self, prefix, glyphs, suffix, lookups, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix
 | |
|         self.lookups = list(lookups)
 | |
|         for i, lookup in enumerate(lookups):
 | |
|             if lookup:
 | |
|                 try:
 | |
|                     iter(lookup)
 | |
|                 except TypeError:
 | |
|                     self.lookups[i] = [lookup]
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder's ``add_chain_context_subst`` callback."""
 | |
|         prefix = [p.glyphSet() for p in self.prefix]
 | |
|         glyphs = [g.glyphSet() for g in self.glyphs]
 | |
|         suffix = [s.glyphSet() for s in self.suffix]
 | |
|         builder.add_chain_context_subst(
 | |
|             self.location, prefix, glyphs, suffix, self.lookups
 | |
|         )
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = "sub "
 | |
|         if (
 | |
|             len(self.prefix)
 | |
|             or len(self.suffix)
 | |
|             or any([x is not None for x in self.lookups])
 | |
|         ):
 | |
|             if len(self.prefix):
 | |
|                 res += " ".join(g.asFea() for g in self.prefix) + " "
 | |
|             for i, g in enumerate(self.glyphs):
 | |
|                 res += g.asFea() + "'"
 | |
|                 if self.lookups[i]:
 | |
|                     for lu in self.lookups[i]:
 | |
|                         res += " lookup " + lu.name
 | |
|                 if i < len(self.glyphs) - 1:
 | |
|                     res += " "
 | |
|             if len(self.suffix):
 | |
|                 res += " " + " ".join(map(asFea, self.suffix))
 | |
|         else:
 | |
|             res += " ".join(map(asFea, self.glyphs))
 | |
|         res += ";"
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class CursivePosStatement(Statement):
 | |
|     """A cursive positioning statement. Entry and exit anchors can either
 | |
|     be :class:`Anchor` objects or ``None``."""
 | |
| 
 | |
|     def __init__(self, glyphclass, entryAnchor, exitAnchor, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.glyphclass = glyphclass
 | |
|         self.entryAnchor, self.exitAnchor = entryAnchor, exitAnchor
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``add_cursive_pos`` callback."""
 | |
|         builder.add_cursive_pos(
 | |
|             self.location, self.glyphclass.glyphSet(), self.entryAnchor, self.exitAnchor
 | |
|         )
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         entry = self.entryAnchor.asFea() if self.entryAnchor else "<anchor NULL>"
 | |
|         exit = self.exitAnchor.asFea() if self.exitAnchor else "<anchor NULL>"
 | |
|         return "pos cursive {} {} {};".format(self.glyphclass.asFea(), entry, exit)
 | |
| 
 | |
| 
 | |
| class FeatureReferenceStatement(Statement):
 | |
|     """Example: ``feature salt;``"""
 | |
| 
 | |
|     def __init__(self, featureName, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.location, self.featureName = (location, featureName)
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``add_feature_reference`` callback."""
 | |
|         builder.add_feature_reference(self.location, self.featureName)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         return "feature {};".format(self.featureName)
 | |
| 
 | |
| 
 | |
| class IgnorePosStatement(Statement):
 | |
|     """An ``ignore pos`` statement, containing `one or more` contexts to ignore.
 | |
| 
 | |
|     ``chainContexts`` should be a list of ``(prefix, glyphs, suffix)`` tuples,
 | |
|     with each of ``prefix``, ``glyphs`` and ``suffix`` being
 | |
|     `glyph-containing objects`_ ."""
 | |
| 
 | |
|     def __init__(self, chainContexts, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.chainContexts = chainContexts
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``add_chain_context_pos`` callback on each
 | |
|         rule context."""
 | |
|         for prefix, glyphs, suffix in self.chainContexts:
 | |
|             prefix = [p.glyphSet() for p in prefix]
 | |
|             glyphs = [g.glyphSet() for g in glyphs]
 | |
|             suffix = [s.glyphSet() for s in suffix]
 | |
|             builder.add_chain_context_pos(self.location, prefix, glyphs, suffix, [])
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         contexts = []
 | |
|         for prefix, glyphs, suffix in self.chainContexts:
 | |
|             res = ""
 | |
|             if len(prefix) or len(suffix):
 | |
|                 if len(prefix):
 | |
|                     res += " ".join(map(asFea, prefix)) + " "
 | |
|                 res += " ".join(g.asFea() + "'" for g in glyphs)
 | |
|                 if len(suffix):
 | |
|                     res += " " + " ".join(map(asFea, suffix))
 | |
|             else:
 | |
|                 res += " ".join(map(asFea, glyphs))
 | |
|             contexts.append(res)
 | |
|         return "ignore pos " + ", ".join(contexts) + ";"
 | |
| 
 | |
| 
 | |
| class IgnoreSubstStatement(Statement):
 | |
|     """An ``ignore sub`` statement, containing `one or more` contexts to ignore.
 | |
| 
 | |
|     ``chainContexts`` should be a list of ``(prefix, glyphs, suffix)`` tuples,
 | |
|     with each of ``prefix``, ``glyphs`` and ``suffix`` being
 | |
|     `glyph-containing objects`_ ."""
 | |
| 
 | |
|     def __init__(self, chainContexts, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.chainContexts = chainContexts
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``add_chain_context_subst`` callback on
 | |
|         each rule context."""
 | |
|         for prefix, glyphs, suffix in self.chainContexts:
 | |
|             prefix = [p.glyphSet() for p in prefix]
 | |
|             glyphs = [g.glyphSet() for g in glyphs]
 | |
|             suffix = [s.glyphSet() for s in suffix]
 | |
|             builder.add_chain_context_subst(self.location, prefix, glyphs, suffix, [])
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         contexts = []
 | |
|         for prefix, glyphs, suffix in self.chainContexts:
 | |
|             res = ""
 | |
|             if len(prefix):
 | |
|                 res += " ".join(map(asFea, prefix)) + " "
 | |
|             res += " ".join(g.asFea() + "'" for g in glyphs)
 | |
|             if len(suffix):
 | |
|                 res += " " + " ".join(map(asFea, suffix))
 | |
|             contexts.append(res)
 | |
|         return "ignore sub " + ", ".join(contexts) + ";"
 | |
| 
 | |
| 
 | |
| class IncludeStatement(Statement):
 | |
|     """An ``include()`` statement."""
 | |
| 
 | |
|     def __init__(self, filename, location=None):
 | |
|         super(IncludeStatement, self).__init__(location)
 | |
|         self.filename = filename  #: String containing name of file to include
 | |
| 
 | |
|     def build(self):
 | |
|         # TODO: consider lazy-loading the including parser/lexer?
 | |
|         raise FeatureLibError(
 | |
|             "Building an include statement is not implemented yet. "
 | |
|             "Instead, use Parser(..., followIncludes=True) for building.",
 | |
|             self.location,
 | |
|         )
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         return indent + "include(%s);" % self.filename
 | |
| 
 | |
| 
 | |
| class LanguageStatement(Statement):
 | |
|     """A ``language`` statement within a feature."""
 | |
| 
 | |
|     def __init__(self, language, include_default=True, required=False, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         assert len(language) == 4
 | |
|         self.language = language  #: A four-character language tag
 | |
|         self.include_default = include_default  #: If false, "exclude_dflt"
 | |
|         self.required = required
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Call the builder object's ``set_language`` callback."""
 | |
|         builder.set_language(
 | |
|             location=self.location,
 | |
|             language=self.language,
 | |
|             include_default=self.include_default,
 | |
|             required=self.required,
 | |
|         )
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = "language {}".format(self.language.strip())
 | |
|         if not self.include_default:
 | |
|             res += " exclude_dflt"
 | |
|         if self.required:
 | |
|             res += " required"
 | |
|         res += ";"
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class LanguageSystemStatement(Statement):
 | |
|     """A top-level ``languagesystem`` statement."""
 | |
| 
 | |
|     def __init__(self, script, language, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.script, self.language = (script, language)
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``add_language_system`` callback."""
 | |
|         builder.add_language_system(self.location, self.script, self.language)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         return "languagesystem {} {};".format(self.script, self.language.strip())
 | |
| 
 | |
| 
 | |
| class FontRevisionStatement(Statement):
 | |
|     """A ``head`` table ``FontRevision`` statement. ``revision`` should be a
 | |
|     number, and will be formatted to three significant decimal places."""
 | |
| 
 | |
|     def __init__(self, revision, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.revision = revision
 | |
| 
 | |
|     def build(self, builder):
 | |
|         builder.set_font_revision(self.location, self.revision)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         return "FontRevision {:.3f};".format(self.revision)
 | |
| 
 | |
| 
 | |
| class LigatureCaretByIndexStatement(Statement):
 | |
|     """A ``GDEF`` table ``LigatureCaretByIndex`` statement. ``glyphs`` should be
 | |
|     a `glyph-containing object`_, and ``carets`` should be a list of integers."""
 | |
| 
 | |
|     def __init__(self, glyphs, carets, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.glyphs, self.carets = (glyphs, carets)
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``add_ligatureCaretByIndex_`` callback."""
 | |
|         glyphs = self.glyphs.glyphSet()
 | |
|         builder.add_ligatureCaretByIndex_(self.location, glyphs, set(self.carets))
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         return "LigatureCaretByIndex {} {};".format(
 | |
|             self.glyphs.asFea(), " ".join(str(x) for x in self.carets)
 | |
|         )
 | |
| 
 | |
| 
 | |
| class LigatureCaretByPosStatement(Statement):
 | |
|     """A ``GDEF`` table ``LigatureCaretByPos`` statement. ``glyphs`` should be
 | |
|     a `glyph-containing object`_, and ``carets`` should be a list of integers."""
 | |
| 
 | |
|     def __init__(self, glyphs, carets, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.glyphs, self.carets = (glyphs, carets)
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``add_ligatureCaretByPos_`` callback."""
 | |
|         glyphs = self.glyphs.glyphSet()
 | |
|         builder.add_ligatureCaretByPos_(self.location, glyphs, set(self.carets))
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         return "LigatureCaretByPos {} {};".format(
 | |
|             self.glyphs.asFea(), " ".join(str(x) for x in self.carets)
 | |
|         )
 | |
| 
 | |
| 
 | |
| class LigatureSubstStatement(Statement):
 | |
|     """A chained contextual substitution statement.
 | |
| 
 | |
|     ``prefix``, ``glyphs``, and ``suffix`` should be lists of
 | |
|     `glyph-containing objects`_; ``replacement`` should be a single
 | |
|     `glyph-containing object`_.
 | |
| 
 | |
|     If ``forceChain`` is True, this is expressed as a chaining rule
 | |
|     (e.g. ``sub f' i' by f_i``) even when no context is given."""
 | |
| 
 | |
|     def __init__(self, prefix, glyphs, suffix, replacement, forceChain, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.prefix, self.glyphs, self.suffix = (prefix, glyphs, suffix)
 | |
|         self.replacement, self.forceChain = replacement, forceChain
 | |
| 
 | |
|     def build(self, builder):
 | |
|         prefix = [p.glyphSet() for p in self.prefix]
 | |
|         glyphs = [g.glyphSet() for g in self.glyphs]
 | |
|         suffix = [s.glyphSet() for s in self.suffix]
 | |
|         builder.add_ligature_subst(
 | |
|             self.location, prefix, glyphs, suffix, self.replacement, self.forceChain
 | |
|         )
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = "sub "
 | |
|         if len(self.prefix) or len(self.suffix) or self.forceChain:
 | |
|             if len(self.prefix):
 | |
|                 res += " ".join(g.asFea() for g in self.prefix) + " "
 | |
|             res += " ".join(g.asFea() + "'" for g in self.glyphs)
 | |
|             if len(self.suffix):
 | |
|                 res += " " + " ".join(g.asFea() for g in self.suffix)
 | |
|         else:
 | |
|             res += " ".join(g.asFea() for g in self.glyphs)
 | |
|         res += " by "
 | |
|         res += asFea(self.replacement)
 | |
|         res += ";"
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class LookupFlagStatement(Statement):
 | |
|     """A ``lookupflag`` statement. The ``value`` should be an integer value
 | |
|     representing the flags in use, but not including the ``markAttachment``
 | |
|     class and ``markFilteringSet`` values, which must be specified as
 | |
|     glyph-containing objects."""
 | |
| 
 | |
|     def __init__(
 | |
|         self, value=0, markAttachment=None, markFilteringSet=None, location=None
 | |
|     ):
 | |
|         Statement.__init__(self, location)
 | |
|         self.value = value
 | |
|         self.markAttachment = markAttachment
 | |
|         self.markFilteringSet = markFilteringSet
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``set_lookup_flag`` callback."""
 | |
|         markAttach = None
 | |
|         if self.markAttachment is not None:
 | |
|             markAttach = self.markAttachment.glyphSet()
 | |
|         markFilter = None
 | |
|         if self.markFilteringSet is not None:
 | |
|             markFilter = self.markFilteringSet.glyphSet()
 | |
|         builder.set_lookup_flag(self.location, self.value, markAttach, markFilter)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = []
 | |
|         flags = ["RightToLeft", "IgnoreBaseGlyphs", "IgnoreLigatures", "IgnoreMarks"]
 | |
|         curr = 1
 | |
|         for i in range(len(flags)):
 | |
|             if self.value & curr != 0:
 | |
|                 res.append(flags[i])
 | |
|             curr = curr << 1
 | |
|         if self.markAttachment is not None:
 | |
|             res.append("MarkAttachmentType {}".format(self.markAttachment.asFea()))
 | |
|         if self.markFilteringSet is not None:
 | |
|             res.append("UseMarkFilteringSet {}".format(self.markFilteringSet.asFea()))
 | |
|         if not res:
 | |
|             res = ["0"]
 | |
|         return "lookupflag {};".format(" ".join(res))
 | |
| 
 | |
| 
 | |
| class LookupReferenceStatement(Statement):
 | |
|     """Represents a ``lookup ...;`` statement to include a lookup in a feature.
 | |
| 
 | |
|     The ``lookup`` should be a :class:`LookupBlock` object."""
 | |
| 
 | |
|     def __init__(self, lookup, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.location, self.lookup = (location, lookup)
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``add_lookup_call`` callback."""
 | |
|         builder.add_lookup_call(self.lookup.name)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         return "lookup {};".format(self.lookup.name)
 | |
| 
 | |
| 
 | |
| class MarkBasePosStatement(Statement):
 | |
|     """A mark-to-base positioning rule. The ``base`` should be a
 | |
|     `glyph-containing object`_. The ``marks`` should be a list of
 | |
|     (:class:`Anchor`, :class:`MarkClass`) tuples."""
 | |
| 
 | |
|     def __init__(self, base, marks, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.base, self.marks = base, marks
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``add_mark_base_pos`` callback."""
 | |
|         builder.add_mark_base_pos(self.location, self.base.glyphSet(), self.marks)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = "pos base {}".format(self.base.asFea())
 | |
|         for a, m in self.marks:
 | |
|             res += "\n" + indent + SHIFT + "{} mark @{}".format(a.asFea(), m.name)
 | |
|         res += ";"
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class MarkLigPosStatement(Statement):
 | |
|     """A mark-to-ligature positioning rule. The ``ligatures`` must be a
 | |
|     `glyph-containing object`_. The ``marks`` should be a list of lists: each
 | |
|     element in the top-level list represents a component glyph, and is made
 | |
|     up of a list of (:class:`Anchor`, :class:`MarkClass`) tuples representing
 | |
|     mark attachment points for that position.
 | |
| 
 | |
|     Example::
 | |
| 
 | |
|         m1 = MarkClass("TOP_MARKS")
 | |
|         m2 = MarkClass("BOTTOM_MARKS")
 | |
|         # ... add definitions to mark classes...
 | |
| 
 | |
|         glyph = GlyphName("lam_meem_jeem")
 | |
|         marks = [
 | |
|             [ (Anchor(625,1800), m1) ], # Attachments on 1st component (lam)
 | |
|             [ (Anchor(376,-378), m2) ], # Attachments on 2nd component (meem)
 | |
|             [ ]                         # No attachments on the jeem
 | |
|         ]
 | |
|         mlp = MarkLigPosStatement(glyph, marks)
 | |
| 
 | |
|         mlp.asFea()
 | |
|         # pos ligature lam_meem_jeem <anchor 625 1800> mark @TOP_MARKS
 | |
|         # ligComponent <anchor 376 -378> mark @BOTTOM_MARKS;
 | |
| 
 | |
|     """
 | |
| 
 | |
|     def __init__(self, ligatures, marks, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.ligatures, self.marks = ligatures, marks
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``add_mark_lig_pos`` callback."""
 | |
|         builder.add_mark_lig_pos(self.location, self.ligatures.glyphSet(), self.marks)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = "pos ligature {}".format(self.ligatures.asFea())
 | |
|         ligs = []
 | |
|         for l in self.marks:
 | |
|             temp = ""
 | |
|             if l is None or not len(l):
 | |
|                 temp = "\n" + indent + SHIFT * 2 + "<anchor NULL>"
 | |
|             else:
 | |
|                 for a, m in l:
 | |
|                     temp += (
 | |
|                         "\n"
 | |
|                         + indent
 | |
|                         + SHIFT * 2
 | |
|                         + "{} mark @{}".format(a.asFea(), m.name)
 | |
|                     )
 | |
|             ligs.append(temp)
 | |
|         res += ("\n" + indent + SHIFT + "ligComponent").join(ligs)
 | |
|         res += ";"
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class MarkMarkPosStatement(Statement):
 | |
|     """A mark-to-mark positioning rule. The ``baseMarks`` must be a
 | |
|     `glyph-containing object`_. The ``marks`` should be a list of
 | |
|     (:class:`Anchor`, :class:`MarkClass`) tuples."""
 | |
| 
 | |
|     def __init__(self, baseMarks, marks, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.baseMarks, self.marks = baseMarks, marks
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``add_mark_mark_pos`` callback."""
 | |
|         builder.add_mark_mark_pos(self.location, self.baseMarks.glyphSet(), self.marks)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = "pos mark {}".format(self.baseMarks.asFea())
 | |
|         for a, m in self.marks:
 | |
|             res += "\n" + indent + SHIFT + "{} mark @{}".format(a.asFea(), m.name)
 | |
|         res += ";"
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class MultipleSubstStatement(Statement):
 | |
|     """A multiple substitution statement.
 | |
| 
 | |
|     Args:
 | |
|         prefix: a list of `glyph-containing objects`_.
 | |
|         glyph: a single glyph-containing object.
 | |
|         suffix: a list of glyph-containing objects.
 | |
|         replacement: a list of glyph-containing objects.
 | |
|         forceChain: If true, the statement is expressed as a chaining rule
 | |
|             (e.g. ``sub f' i' by f_i``) even when no context is given.
 | |
|     """
 | |
| 
 | |
|     def __init__(
 | |
|         self, prefix, glyph, suffix, replacement, forceChain=False, location=None
 | |
|     ):
 | |
|         Statement.__init__(self, location)
 | |
|         self.prefix, self.glyph, self.suffix = prefix, glyph, suffix
 | |
|         self.replacement = replacement
 | |
|         self.forceChain = forceChain
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``add_multiple_subst`` callback."""
 | |
|         prefix = [p.glyphSet() for p in self.prefix]
 | |
|         suffix = [s.glyphSet() for s in self.suffix]
 | |
|         if hasattr(self.glyph, "glyphSet"):
 | |
|             originals = self.glyph.glyphSet()
 | |
|         else:
 | |
|             originals = [self.glyph]
 | |
|         count = len(originals)
 | |
|         replaces = []
 | |
|         for r in self.replacement:
 | |
|             if hasattr(r, "glyphSet"):
 | |
|                 replace = r.glyphSet()
 | |
|             else:
 | |
|                 replace = [r]
 | |
|             if len(replace) == 1 and len(replace) != count:
 | |
|                 replace = replace * count
 | |
|             replaces.append(replace)
 | |
|         replaces = list(zip(*replaces))
 | |
| 
 | |
|         seen_originals = set()
 | |
|         for i, original in enumerate(originals):
 | |
|             if original not in seen_originals:
 | |
|                 seen_originals.add(original)
 | |
|                 builder.add_multiple_subst(
 | |
|                     self.location,
 | |
|                     prefix,
 | |
|                     original,
 | |
|                     suffix,
 | |
|                     replaces and replaces[i] or (),
 | |
|                     self.forceChain,
 | |
|                 )
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = "sub "
 | |
|         if len(self.prefix) or len(self.suffix) or self.forceChain:
 | |
|             if len(self.prefix):
 | |
|                 res += " ".join(map(asFea, self.prefix)) + " "
 | |
|             res += asFea(self.glyph) + "'"
 | |
|             if len(self.suffix):
 | |
|                 res += " " + " ".join(map(asFea, self.suffix))
 | |
|         else:
 | |
|             res += asFea(self.glyph)
 | |
|         replacement = self.replacement or [NullGlyph()]
 | |
|         res += " by "
 | |
|         res += " ".join(map(asFea, replacement))
 | |
|         res += ";"
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class PairPosStatement(Statement):
 | |
|     """A pair positioning statement.
 | |
| 
 | |
|     ``glyphs1`` and ``glyphs2`` should be `glyph-containing objects`_.
 | |
|     ``valuerecord1`` should be a :class:`ValueRecord` object;
 | |
|     ``valuerecord2`` should be either a :class:`ValueRecord` object or ``None``.
 | |
|     If ``enumerated`` is true, then this is expressed as an
 | |
|     `enumerated pair <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#6.b.ii>`_.
 | |
|     """
 | |
| 
 | |
|     def __init__(
 | |
|         self,
 | |
|         glyphs1,
 | |
|         valuerecord1,
 | |
|         glyphs2,
 | |
|         valuerecord2,
 | |
|         enumerated=False,
 | |
|         location=None,
 | |
|     ):
 | |
|         Statement.__init__(self, location)
 | |
|         self.enumerated = enumerated
 | |
|         self.glyphs1, self.valuerecord1 = glyphs1, valuerecord1
 | |
|         self.glyphs2, self.valuerecord2 = glyphs2, valuerecord2
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls a callback on the builder object:
 | |
| 
 | |
|         * If the rule is enumerated, calls ``add_specific_pair_pos`` on each
 | |
|           combination of first and second glyphs.
 | |
|         * If the glyphs are both single :class:`GlyphName` objects, calls
 | |
|           ``add_specific_pair_pos``.
 | |
|         * Else, calls ``add_class_pair_pos``.
 | |
|         """
 | |
|         if self.enumerated:
 | |
|             g = [self.glyphs1.glyphSet(), self.glyphs2.glyphSet()]
 | |
|             seen_pair = False
 | |
|             for glyph1, glyph2 in itertools.product(*g):
 | |
|                 seen_pair = True
 | |
|                 builder.add_specific_pair_pos(
 | |
|                     self.location, glyph1, self.valuerecord1, glyph2, self.valuerecord2
 | |
|                 )
 | |
|             if not seen_pair:
 | |
|                 raise FeatureLibError(
 | |
|                     "Empty glyph class in positioning rule", self.location
 | |
|                 )
 | |
|             return
 | |
| 
 | |
|         is_specific = isinstance(self.glyphs1, GlyphName) and isinstance(
 | |
|             self.glyphs2, GlyphName
 | |
|         )
 | |
|         if is_specific:
 | |
|             builder.add_specific_pair_pos(
 | |
|                 self.location,
 | |
|                 self.glyphs1.glyph,
 | |
|                 self.valuerecord1,
 | |
|                 self.glyphs2.glyph,
 | |
|                 self.valuerecord2,
 | |
|             )
 | |
|         else:
 | |
|             builder.add_class_pair_pos(
 | |
|                 self.location,
 | |
|                 self.glyphs1.glyphSet(),
 | |
|                 self.valuerecord1,
 | |
|                 self.glyphs2.glyphSet(),
 | |
|                 self.valuerecord2,
 | |
|             )
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = "enum " if self.enumerated else ""
 | |
|         if self.valuerecord2:
 | |
|             res += "pos {} {} {} {};".format(
 | |
|                 self.glyphs1.asFea(),
 | |
|                 self.valuerecord1.asFea(),
 | |
|                 self.glyphs2.asFea(),
 | |
|                 self.valuerecord2.asFea(),
 | |
|             )
 | |
|         else:
 | |
|             res += "pos {} {} {};".format(
 | |
|                 self.glyphs1.asFea(), self.glyphs2.asFea(), self.valuerecord1.asFea()
 | |
|             )
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class ReverseChainSingleSubstStatement(Statement):
 | |
|     """A reverse chaining substitution statement. You don't see those every day.
 | |
| 
 | |
|     Note the unusual argument order: ``suffix`` comes `before` ``glyphs``.
 | |
|     ``old_prefix``, ``old_suffix``, ``glyphs`` and ``replacements`` should be
 | |
|     lists of `glyph-containing objects`_. ``glyphs`` and ``replacements`` should
 | |
|     be one-item lists.
 | |
|     """
 | |
| 
 | |
|     def __init__(self, old_prefix, old_suffix, glyphs, replacements, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.old_prefix, self.old_suffix = old_prefix, old_suffix
 | |
|         self.glyphs = glyphs
 | |
|         self.replacements = replacements
 | |
| 
 | |
|     def build(self, builder):
 | |
|         prefix = [p.glyphSet() for p in self.old_prefix]
 | |
|         suffix = [s.glyphSet() for s in self.old_suffix]
 | |
|         originals = self.glyphs[0].glyphSet()
 | |
|         replaces = self.replacements[0].glyphSet()
 | |
|         if len(replaces) == 1:
 | |
|             replaces = replaces * len(originals)
 | |
|         builder.add_reverse_chain_single_subst(
 | |
|             self.location, prefix, suffix, dict(zip(originals, replaces))
 | |
|         )
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = "rsub "
 | |
|         if len(self.old_prefix) or len(self.old_suffix):
 | |
|             if len(self.old_prefix):
 | |
|                 res += " ".join(asFea(g) for g in self.old_prefix) + " "
 | |
|             res += " ".join(asFea(g) + "'" for g in self.glyphs)
 | |
|             if len(self.old_suffix):
 | |
|                 res += " " + " ".join(asFea(g) for g in self.old_suffix)
 | |
|         else:
 | |
|             res += " ".join(map(asFea, self.glyphs))
 | |
|         res += " by {};".format(" ".join(asFea(g) for g in self.replacements))
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class SingleSubstStatement(Statement):
 | |
|     """A single substitution statement.
 | |
| 
 | |
|     Note the unusual argument order: ``prefix`` and suffix come `after`
 | |
|     the replacement ``glyphs``. ``prefix``, ``suffix``, ``glyphs`` and
 | |
|     ``replace`` should be lists of `glyph-containing objects`_. ``glyphs`` and
 | |
|     ``replace`` should be one-item lists.
 | |
|     """
 | |
| 
 | |
|     def __init__(self, glyphs, replace, prefix, suffix, forceChain, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.prefix, self.suffix = prefix, suffix
 | |
|         self.forceChain = forceChain
 | |
|         self.glyphs = glyphs
 | |
|         self.replacements = replace
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``add_single_subst`` callback."""
 | |
|         prefix = [p.glyphSet() for p in self.prefix]
 | |
|         suffix = [s.glyphSet() for s in self.suffix]
 | |
|         originals = self.glyphs[0].glyphSet()
 | |
|         replaces = self.replacements[0].glyphSet()
 | |
|         if len(replaces) == 1:
 | |
|             replaces = replaces * len(originals)
 | |
|         builder.add_single_subst(
 | |
|             self.location,
 | |
|             prefix,
 | |
|             suffix,
 | |
|             OrderedDict(zip(originals, replaces)),
 | |
|             self.forceChain,
 | |
|         )
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = "sub "
 | |
|         if len(self.prefix) or len(self.suffix) or self.forceChain:
 | |
|             if len(self.prefix):
 | |
|                 res += " ".join(asFea(g) for g in self.prefix) + " "
 | |
|             res += " ".join(asFea(g) + "'" for g in self.glyphs)
 | |
|             if len(self.suffix):
 | |
|                 res += " " + " ".join(asFea(g) for g in self.suffix)
 | |
|         else:
 | |
|             res += " ".join(asFea(g) for g in self.glyphs)
 | |
|         res += " by {};".format(" ".join(asFea(g) for g in self.replacements))
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class ScriptStatement(Statement):
 | |
|     """A ``script`` statement."""
 | |
| 
 | |
|     def __init__(self, script, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.script = script  #: the script code
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder's ``set_script`` callback."""
 | |
|         builder.set_script(self.location, self.script)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         return "script {};".format(self.script.strip())
 | |
| 
 | |
| 
 | |
| class SinglePosStatement(Statement):
 | |
|     """A single position statement. ``prefix`` and ``suffix`` should be
 | |
|     lists of `glyph-containing objects`_.
 | |
| 
 | |
|     ``pos`` should be a one-element list containing a (`glyph-containing object`_,
 | |
|     :class:`ValueRecord`) tuple."""
 | |
| 
 | |
|     def __init__(self, pos, prefix, suffix, forceChain, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.pos, self.prefix, self.suffix = pos, prefix, suffix
 | |
|         self.forceChain = forceChain
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``add_single_pos`` callback."""
 | |
|         prefix = [p.glyphSet() for p in self.prefix]
 | |
|         suffix = [s.glyphSet() for s in self.suffix]
 | |
|         pos = [(g.glyphSet(), value) for g, value in self.pos]
 | |
|         builder.add_single_pos(self.location, prefix, suffix, pos, self.forceChain)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = "pos "
 | |
|         if len(self.prefix) or len(self.suffix) or self.forceChain:
 | |
|             if len(self.prefix):
 | |
|                 res += " ".join(map(asFea, self.prefix)) + " "
 | |
|             res += " ".join(
 | |
|                 [
 | |
|                     asFea(x[0])
 | |
|                     + "'"
 | |
|                     + ((" " + x[1].asFea()) if x[1] is not None else "")
 | |
|                     for x in self.pos
 | |
|                 ]
 | |
|             )
 | |
|             if len(self.suffix):
 | |
|                 res += " " + " ".join(map(asFea, self.suffix))
 | |
|         else:
 | |
|             res += " ".join(
 | |
|                 [
 | |
|                     asFea(x[0]) + " " + (x[1].asFea() if x[1] is not None else "")
 | |
|                     for x in self.pos
 | |
|                 ]
 | |
|             )
 | |
|         res += ";"
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class SubtableStatement(Statement):
 | |
|     """Represents a subtable break."""
 | |
| 
 | |
|     def __init__(self, location=None):
 | |
|         Statement.__init__(self, location)
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder objects's ``add_subtable_break`` callback."""
 | |
|         builder.add_subtable_break(self.location)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         return "subtable;"
 | |
| 
 | |
| 
 | |
| class ValueRecord(Expression):
 | |
|     """Represents a value record."""
 | |
| 
 | |
|     def __init__(
 | |
|         self,
 | |
|         xPlacement=None,
 | |
|         yPlacement=None,
 | |
|         xAdvance=None,
 | |
|         yAdvance=None,
 | |
|         xPlaDevice=None,
 | |
|         yPlaDevice=None,
 | |
|         xAdvDevice=None,
 | |
|         yAdvDevice=None,
 | |
|         vertical=False,
 | |
|         location=None,
 | |
|     ):
 | |
|         Expression.__init__(self, location)
 | |
|         self.xPlacement, self.yPlacement = (xPlacement, yPlacement)
 | |
|         self.xAdvance, self.yAdvance = (xAdvance, yAdvance)
 | |
|         self.xPlaDevice, self.yPlaDevice = (xPlaDevice, yPlaDevice)
 | |
|         self.xAdvDevice, self.yAdvDevice = (xAdvDevice, yAdvDevice)
 | |
|         self.vertical = vertical
 | |
| 
 | |
|     def __eq__(self, other):
 | |
|         return (
 | |
|             self.xPlacement == other.xPlacement
 | |
|             and self.yPlacement == other.yPlacement
 | |
|             and self.xAdvance == other.xAdvance
 | |
|             and self.yAdvance == other.yAdvance
 | |
|             and self.xPlaDevice == other.xPlaDevice
 | |
|             and self.xAdvDevice == other.xAdvDevice
 | |
|         )
 | |
| 
 | |
|     def __ne__(self, other):
 | |
|         return not self.__eq__(other)
 | |
| 
 | |
|     def __hash__(self):
 | |
|         return (
 | |
|             hash(self.xPlacement)
 | |
|             ^ hash(self.yPlacement)
 | |
|             ^ hash(self.xAdvance)
 | |
|             ^ hash(self.yAdvance)
 | |
|             ^ hash(self.xPlaDevice)
 | |
|             ^ hash(self.yPlaDevice)
 | |
|             ^ hash(self.xAdvDevice)
 | |
|             ^ hash(self.yAdvDevice)
 | |
|         )
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         if not self:
 | |
|             return "<NULL>"
 | |
| 
 | |
|         x, y = self.xPlacement, self.yPlacement
 | |
|         xAdvance, yAdvance = self.xAdvance, self.yAdvance
 | |
|         xPlaDevice, yPlaDevice = self.xPlaDevice, self.yPlaDevice
 | |
|         xAdvDevice, yAdvDevice = self.xAdvDevice, self.yAdvDevice
 | |
|         vertical = self.vertical
 | |
| 
 | |
|         # Try format A, if possible.
 | |
|         if x is None and y is None:
 | |
|             if xAdvance is None and vertical:
 | |
|                 return str(yAdvance)
 | |
|             elif yAdvance is None and not vertical:
 | |
|                 return str(xAdvance)
 | |
| 
 | |
|         # Make any remaining None value 0 to avoid generating invalid records.
 | |
|         x = x or 0
 | |
|         y = y or 0
 | |
|         xAdvance = xAdvance or 0
 | |
|         yAdvance = yAdvance or 0
 | |
| 
 | |
|         # Try format B, if possible.
 | |
|         if (
 | |
|             xPlaDevice is None
 | |
|             and yPlaDevice is None
 | |
|             and xAdvDevice is None
 | |
|             and yAdvDevice is None
 | |
|         ):
 | |
|             return "<%s %s %s %s>" % (x, y, xAdvance, yAdvance)
 | |
| 
 | |
|         # Last resort is format C.
 | |
|         return "<%s %s %s %s %s %s %s %s>" % (
 | |
|             x,
 | |
|             y,
 | |
|             xAdvance,
 | |
|             yAdvance,
 | |
|             deviceToString(xPlaDevice),
 | |
|             deviceToString(yPlaDevice),
 | |
|             deviceToString(xAdvDevice),
 | |
|             deviceToString(yAdvDevice),
 | |
|         )
 | |
| 
 | |
|     def __bool__(self):
 | |
|         return any(
 | |
|             getattr(self, v) is not None
 | |
|             for v in [
 | |
|                 "xPlacement",
 | |
|                 "yPlacement",
 | |
|                 "xAdvance",
 | |
|                 "yAdvance",
 | |
|                 "xPlaDevice",
 | |
|                 "yPlaDevice",
 | |
|                 "xAdvDevice",
 | |
|                 "yAdvDevice",
 | |
|             ]
 | |
|         )
 | |
| 
 | |
|     __nonzero__ = __bool__
 | |
| 
 | |
| 
 | |
| class ValueRecordDefinition(Statement):
 | |
|     """Represents a named value record definition."""
 | |
| 
 | |
|     def __init__(self, name, value, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.name = name  #: Value record name as string
 | |
|         self.value = value  #: :class:`ValueRecord` object
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         return "valueRecordDef {} {};".format(self.value.asFea(), self.name)
 | |
| 
 | |
| 
 | |
| def simplify_name_attributes(pid, eid, lid):
 | |
|     if pid == 3 and eid == 1 and lid == 1033:
 | |
|         return ""
 | |
|     elif pid == 1 and eid == 0 and lid == 0:
 | |
|         return "1"
 | |
|     else:
 | |
|         return "{} {} {}".format(pid, eid, lid)
 | |
| 
 | |
| 
 | |
| class NameRecord(Statement):
 | |
|     """Represents a name record. (`Section 9.e. <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.e>`_)"""
 | |
| 
 | |
|     def __init__(self, nameID, platformID, platEncID, langID, string, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.nameID = nameID  #: Name ID as integer (e.g. 9 for designer's name)
 | |
|         self.platformID = platformID  #: Platform ID as integer
 | |
|         self.platEncID = platEncID  #: Platform encoding ID as integer
 | |
|         self.langID = langID  #: Language ID as integer
 | |
|         self.string = string  #: Name record value
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``add_name_record`` callback."""
 | |
|         builder.add_name_record(
 | |
|             self.location,
 | |
|             self.nameID,
 | |
|             self.platformID,
 | |
|             self.platEncID,
 | |
|             self.langID,
 | |
|             self.string,
 | |
|         )
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         def escape(c, escape_pattern):
 | |
|             # Also escape U+0022 QUOTATION MARK and U+005C REVERSE SOLIDUS
 | |
|             if c >= 0x20 and c <= 0x7E and c not in (0x22, 0x5C):
 | |
|                 return chr(c)
 | |
|             else:
 | |
|                 return escape_pattern % c
 | |
| 
 | |
|         encoding = getEncoding(self.platformID, self.platEncID, self.langID)
 | |
|         if encoding is None:
 | |
|             raise FeatureLibError("Unsupported encoding", self.location)
 | |
|         s = tobytes(self.string, encoding=encoding)
 | |
|         if encoding == "utf_16_be":
 | |
|             escaped_string = "".join(
 | |
|                 [
 | |
|                     escape(byteord(s[i]) * 256 + byteord(s[i + 1]), r"\%04x")
 | |
|                     for i in range(0, len(s), 2)
 | |
|                 ]
 | |
|             )
 | |
|         else:
 | |
|             escaped_string = "".join([escape(byteord(b), r"\%02x") for b in s])
 | |
|         plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID)
 | |
|         if plat != "":
 | |
|             plat += " "
 | |
|         return 'nameid {} {}"{}";'.format(self.nameID, plat, escaped_string)
 | |
| 
 | |
| 
 | |
| class FeatureNameStatement(NameRecord):
 | |
|     """Represents a ``sizemenuname`` or ``name`` statement."""
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``add_featureName`` callback."""
 | |
|         NameRecord.build(self, builder)
 | |
|         builder.add_featureName(self.nameID)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         if self.nameID == "size":
 | |
|             tag = "sizemenuname"
 | |
|         else:
 | |
|             tag = "name"
 | |
|         plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID)
 | |
|         if plat != "":
 | |
|             plat += " "
 | |
|         return '{} {}"{}";'.format(tag, plat, self.string)
 | |
| 
 | |
| 
 | |
| class STATNameStatement(NameRecord):
 | |
|     """Represents a STAT table ``name`` statement."""
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID)
 | |
|         if plat != "":
 | |
|             plat += " "
 | |
|         return 'name {}"{}";'.format(plat, self.string)
 | |
| 
 | |
| 
 | |
| class SizeParameters(Statement):
 | |
|     """A ``parameters`` statement."""
 | |
| 
 | |
|     def __init__(self, DesignSize, SubfamilyID, RangeStart, RangeEnd, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.DesignSize = DesignSize
 | |
|         self.SubfamilyID = SubfamilyID
 | |
|         self.RangeStart = RangeStart
 | |
|         self.RangeEnd = RangeEnd
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``set_size_parameters`` callback."""
 | |
|         builder.set_size_parameters(
 | |
|             self.location,
 | |
|             self.DesignSize,
 | |
|             self.SubfamilyID,
 | |
|             self.RangeStart,
 | |
|             self.RangeEnd,
 | |
|         )
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = "parameters {:.1f} {}".format(self.DesignSize, self.SubfamilyID)
 | |
|         if self.RangeStart != 0 or self.RangeEnd != 0:
 | |
|             res += " {} {}".format(int(self.RangeStart * 10), int(self.RangeEnd * 10))
 | |
|         return res + ";"
 | |
| 
 | |
| 
 | |
| class CVParametersNameStatement(NameRecord):
 | |
|     """Represent a name statement inside a ``cvParameters`` block."""
 | |
| 
 | |
|     def __init__(
 | |
|         self, nameID, platformID, platEncID, langID, string, block_name, location=None
 | |
|     ):
 | |
|         NameRecord.__init__(
 | |
|             self, nameID, platformID, platEncID, langID, string, location=location
 | |
|         )
 | |
|         self.block_name = block_name
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``add_cv_parameter`` callback."""
 | |
|         item = ""
 | |
|         if self.block_name == "ParamUILabelNameID":
 | |
|             item = "_{}".format(builder.cv_num_named_params_.get(self.nameID, 0))
 | |
|         builder.add_cv_parameter(self.nameID)
 | |
|         self.nameID = (self.nameID, self.block_name + item)
 | |
|         NameRecord.build(self, builder)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID)
 | |
|         if plat != "":
 | |
|             plat += " "
 | |
|         return 'name {}"{}";'.format(plat, self.string)
 | |
| 
 | |
| 
 | |
| class CharacterStatement(Statement):
 | |
|     """
 | |
|     Statement used in cvParameters blocks of Character Variant features (cvXX).
 | |
|     The Unicode value may be written with either decimal or hexadecimal
 | |
|     notation. The value must be preceded by '0x' if it is a hexadecimal value.
 | |
|     The largest Unicode value allowed is 0xFFFFFF.
 | |
|     """
 | |
| 
 | |
|     def __init__(self, character, tag, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.character = character
 | |
|         self.tag = tag
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``add_cv_character`` callback."""
 | |
|         builder.add_cv_character(self.character, self.tag)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         return "Character {:#x};".format(self.character)
 | |
| 
 | |
| 
 | |
| class BaseAxis(Statement):
 | |
|     """An axis definition, being either a ``VertAxis.BaseTagList/BaseScriptList``
 | |
|     pair or a ``HorizAxis.BaseTagList/BaseScriptList`` pair."""
 | |
| 
 | |
|     def __init__(self, bases, scripts, vertical, minmax=None, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.bases = bases  #: A list of baseline tag names as strings
 | |
|         self.scripts = scripts  #: A list of script record tuplets (script tag, default baseline tag, base coordinate)
 | |
|         self.vertical = vertical  #: Boolean; VertAxis if True, HorizAxis if False
 | |
|         self.minmax = []  #: A set of minmax record
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``set_base_axis`` callback."""
 | |
|         builder.set_base_axis(self.bases, self.scripts, self.vertical, self.minmax)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         direction = "Vert" if self.vertical else "Horiz"
 | |
|         scripts = [
 | |
|             "{} {} {}".format(a[0], a[1], " ".join(map(str, a[2])))
 | |
|             for a in self.scripts
 | |
|         ]
 | |
|         minmaxes = [
 | |
|             "\n{}Axis.MinMax {} {} {}, {};".format(direction, a[0], a[1], a[2], a[3])
 | |
|             for a in self.minmax
 | |
|         ]
 | |
|         return "{}Axis.BaseTagList {};\n{}{}Axis.BaseScriptList {};".format(
 | |
|             direction, " ".join(self.bases), indent, direction, ", ".join(scripts)
 | |
|         ) + "\n".join(minmaxes)
 | |
| 
 | |
| 
 | |
| class OS2Field(Statement):
 | |
|     """An entry in the ``OS/2`` table. Most ``values`` should be numbers or
 | |
|     strings, apart from when the key is ``UnicodeRange``, ``CodePageRange``
 | |
|     or ``Panose``, in which case it should be an array of integers."""
 | |
| 
 | |
|     def __init__(self, key, value, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.key = key
 | |
|         self.value = value
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``add_os2_field`` callback."""
 | |
|         builder.add_os2_field(self.key, self.value)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         def intarr2str(x):
 | |
|             return " ".join(map(str, x))
 | |
| 
 | |
|         numbers = (
 | |
|             "FSType",
 | |
|             "TypoAscender",
 | |
|             "TypoDescender",
 | |
|             "TypoLineGap",
 | |
|             "winAscent",
 | |
|             "winDescent",
 | |
|             "XHeight",
 | |
|             "CapHeight",
 | |
|             "WeightClass",
 | |
|             "WidthClass",
 | |
|             "LowerOpSize",
 | |
|             "UpperOpSize",
 | |
|         )
 | |
|         ranges = ("UnicodeRange", "CodePageRange")
 | |
|         keywords = dict([(x.lower(), [x, str]) for x in numbers])
 | |
|         keywords.update([(x.lower(), [x, intarr2str]) for x in ranges])
 | |
|         keywords["panose"] = ["Panose", intarr2str]
 | |
|         keywords["vendor"] = ["Vendor", lambda y: '"{}"'.format(y)]
 | |
|         if self.key in keywords:
 | |
|             return "{} {};".format(
 | |
|                 keywords[self.key][0], keywords[self.key][1](self.value)
 | |
|             )
 | |
|         return ""  # should raise exception
 | |
| 
 | |
| 
 | |
| class HheaField(Statement):
 | |
|     """An entry in the ``hhea`` table."""
 | |
| 
 | |
|     def __init__(self, key, value, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.key = key
 | |
|         self.value = value
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``add_hhea_field`` callback."""
 | |
|         builder.add_hhea_field(self.key, self.value)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         fields = ("CaretOffset", "Ascender", "Descender", "LineGap")
 | |
|         keywords = dict([(x.lower(), x) for x in fields])
 | |
|         return "{} {};".format(keywords[self.key], self.value)
 | |
| 
 | |
| 
 | |
| class VheaField(Statement):
 | |
|     """An entry in the ``vhea`` table."""
 | |
| 
 | |
|     def __init__(self, key, value, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.key = key
 | |
|         self.value = value
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Calls the builder object's ``add_vhea_field`` callback."""
 | |
|         builder.add_vhea_field(self.key, self.value)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         fields = ("VertTypoAscender", "VertTypoDescender", "VertTypoLineGap")
 | |
|         keywords = dict([(x.lower(), x) for x in fields])
 | |
|         return "{} {};".format(keywords[self.key], self.value)
 | |
| 
 | |
| 
 | |
| class STATDesignAxisStatement(Statement):
 | |
|     """A STAT table Design Axis
 | |
| 
 | |
|     Args:
 | |
|         tag (str): a 4 letter axis tag
 | |
|         axisOrder (int): an int
 | |
|         names (list): a list of :class:`STATNameStatement` objects
 | |
|     """
 | |
| 
 | |
|     def __init__(self, tag, axisOrder, names, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.tag = tag
 | |
|         self.axisOrder = axisOrder
 | |
|         self.names = names
 | |
|         self.location = location
 | |
| 
 | |
|     def build(self, builder):
 | |
|         builder.addDesignAxis(self, self.location)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         indent += SHIFT
 | |
|         res = f"DesignAxis {self.tag} {self.axisOrder} {{ \n"
 | |
|         res += ("\n" + indent).join([s.asFea(indent=indent) for s in self.names]) + "\n"
 | |
|         res += "};"
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class ElidedFallbackName(Statement):
 | |
|     """STAT table ElidedFallbackName
 | |
| 
 | |
|     Args:
 | |
|         names: a list of :class:`STATNameStatement` objects
 | |
|     """
 | |
| 
 | |
|     def __init__(self, names, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.names = names
 | |
|         self.location = location
 | |
| 
 | |
|     def build(self, builder):
 | |
|         builder.setElidedFallbackName(self.names, self.location)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         indent += SHIFT
 | |
|         res = "ElidedFallbackName { \n"
 | |
|         res += ("\n" + indent).join([s.asFea(indent=indent) for s in self.names]) + "\n"
 | |
|         res += "};"
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class ElidedFallbackNameID(Statement):
 | |
|     """STAT table ElidedFallbackNameID
 | |
| 
 | |
|     Args:
 | |
|         value: an int pointing to an existing name table name ID
 | |
|     """
 | |
| 
 | |
|     def __init__(self, value, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.value = value
 | |
|         self.location = location
 | |
| 
 | |
|     def build(self, builder):
 | |
|         builder.setElidedFallbackName(self.value, self.location)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         return f"ElidedFallbackNameID {self.value};"
 | |
| 
 | |
| 
 | |
| class STATAxisValueStatement(Statement):
 | |
|     """A STAT table Axis Value Record
 | |
| 
 | |
|     Args:
 | |
|         names (list): a list of :class:`STATNameStatement` objects
 | |
|         locations (list): a list of :class:`AxisValueLocationStatement` objects
 | |
|         flags (int): an int
 | |
|     """
 | |
| 
 | |
|     def __init__(self, names, locations, flags, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.names = names
 | |
|         self.locations = locations
 | |
|         self.flags = flags
 | |
| 
 | |
|     def build(self, builder):
 | |
|         builder.addAxisValueRecord(self, self.location)
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = "AxisValue {\n"
 | |
|         for location in self.locations:
 | |
|             res += location.asFea()
 | |
| 
 | |
|         for nameRecord in self.names:
 | |
|             res += nameRecord.asFea()
 | |
|             res += "\n"
 | |
| 
 | |
|         if self.flags:
 | |
|             flags = ["OlderSiblingFontAttribute", "ElidableAxisValueName"]
 | |
|             flagStrings = []
 | |
|             curr = 1
 | |
|             for i in range(len(flags)):
 | |
|                 if self.flags & curr != 0:
 | |
|                     flagStrings.append(flags[i])
 | |
|                 curr = curr << 1
 | |
|             res += f"flag {' '.join(flagStrings)};\n"
 | |
|         res += "};"
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class AxisValueLocationStatement(Statement):
 | |
|     """
 | |
|     A STAT table Axis Value Location
 | |
| 
 | |
|     Args:
 | |
|         tag (str): a 4 letter axis tag
 | |
|         values (list): a list of ints and/or floats
 | |
|     """
 | |
| 
 | |
|     def __init__(self, tag, values, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.tag = tag
 | |
|         self.values = values
 | |
| 
 | |
|     def asFea(self, res=""):
 | |
|         res += f"location {self.tag} "
 | |
|         res += f"{' '.join(str(i) for i in self.values)};\n"
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class ConditionsetStatement(Statement):
 | |
|     """
 | |
|     A variable layout conditionset
 | |
| 
 | |
|     Args:
 | |
|         name (str): the name of this conditionset
 | |
|         conditions (dict): a dictionary mapping axis tags to a
 | |
|             tuple of (min,max) userspace coordinates.
 | |
|     """
 | |
| 
 | |
|     def __init__(self, name, conditions, location=None):
 | |
|         Statement.__init__(self, location)
 | |
|         self.name = name
 | |
|         self.conditions = conditions
 | |
| 
 | |
|     def build(self, builder):
 | |
|         builder.add_conditionset(self.location, self.name, self.conditions)
 | |
| 
 | |
|     def asFea(self, res="", indent=""):
 | |
|         res += indent + f"conditionset {self.name} " + "{\n"
 | |
|         for tag, (minvalue, maxvalue) in self.conditions.items():
 | |
|             res += indent + SHIFT + f"{tag} {minvalue} {maxvalue};\n"
 | |
|         res += indent + "}" + f" {self.name};\n"
 | |
|         return res
 | |
| 
 | |
| 
 | |
| class VariationBlock(Block):
 | |
|     """A variation feature block, applicable in a given set of conditions."""
 | |
| 
 | |
|     def __init__(self, name, conditionset, use_extension=False, location=None):
 | |
|         Block.__init__(self, location)
 | |
|         self.name, self.conditionset, self.use_extension = (
 | |
|             name,
 | |
|             conditionset,
 | |
|             use_extension,
 | |
|         )
 | |
| 
 | |
|     def build(self, builder):
 | |
|         """Call the ``start_feature`` callback on the builder object, visit
 | |
|         all the statements in this feature, and then call ``end_feature``."""
 | |
|         builder.start_feature(self.location, self.name, self.use_extension)
 | |
|         if (
 | |
|             self.conditionset != "NULL"
 | |
|             and self.conditionset not in builder.conditionsets_
 | |
|         ):
 | |
|             raise FeatureLibError(
 | |
|                 f"variation block used undefined conditionset {self.conditionset}",
 | |
|                 self.location,
 | |
|             )
 | |
| 
 | |
|         # language exclude_dflt statements modify builder.features_
 | |
|         # limit them to this block with temporary builder.features_
 | |
|         features = builder.features_
 | |
|         builder.features_ = {}
 | |
|         Block.build(self, builder)
 | |
|         for key, value in builder.features_.items():
 | |
|             items = builder.feature_variations_.setdefault(key, {}).setdefault(
 | |
|                 self.conditionset, []
 | |
|             )
 | |
|             items.extend(value)
 | |
|             if key not in features:
 | |
|                 features[key] = []  # Ensure we make a feature record
 | |
|         builder.features_ = features
 | |
|         builder.end_feature()
 | |
| 
 | |
|     def asFea(self, indent=""):
 | |
|         res = indent + "variation %s " % self.name.strip()
 | |
|         res += self.conditionset + " "
 | |
|         if self.use_extension:
 | |
|             res += "useExtension "
 | |
|         res += "{\n"
 | |
|         res += Block.asFea(self, indent=indent)
 | |
|         res += indent + "} %s;\n" % self.name.strip()
 | |
|         return res
 |