Spaces:
Sleeping
Sleeping
| 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 | |