[docs] Document feaLib * Rearrange docs by user intention, highlighting the things you can do with each component. * Remove reference to lexer and error modules from documentation tree, since they’re not user-facing. * I’ve added docstrings to the parser even though we only provide access to the user-facing part of the API in the main documentation, just to clarify what some of the more obscure methods do and provide links to the spec. * AST *is* user-facing if you’re building your own feature files in code, so all classes are documented with the user in mind.
1652 lines
62 KiB
Python
1652 lines
62 KiB
Python
from fontTools.misc.py23 import *
|
|
from fontTools.feaLib.error import FeatureLibError
|
|
from fontTools.misc.encodingTools import getEncoding
|
|
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',
|
|
'BaseAxis',
|
|
'CVParametersNameStatement',
|
|
'ChainContextPosStatement',
|
|
'ChainContextSubstStatement',
|
|
'CharacterStatement',
|
|
'CursivePosStatement',
|
|
'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',
|
|
'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 - tuple of ``(filename, line, column)``
|
|
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 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``."""
|
|
# TODO(sascha): Handle use_extension.
|
|
builder.start_feature(self.location, self.name)
|
|
# 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):
|
|
# TODO(sascha): Handle use_extension.
|
|
builder.start_lookup_block(self.location, self.name)
|
|
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(definition)
|
|
for glyph in definition.glyphSet():
|
|
if glyph in self.glyphs:
|
|
otherLoc = self.glyphs[glyph].location
|
|
if otherLoc is None:
|
|
end = ""
|
|
else:
|
|
end = " at %s:%d:%d" % (
|
|
otherLoc[0], otherLoc[1], otherLoc[2])
|
|
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.
|
|
|
|
``prefix``, ``glyph``, ``suffix`` and ``replacement`` should be lists of
|
|
`glyph-containing objects`_. ``glyph`` should be a `one element list`."""
|
|
|
|
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):
|
|
"""A chained contextual positioning statement.
|
|
|
|
``prefix``, ``glyphs``, and ``suffix`` should be lists of
|
|
`glyph-containing objects`_ .
|
|
|
|
``lookups`` should be a list of lists containing :class:`LookupBlock`
|
|
statements. The length of the outer list should equal to the length of
|
|
``glyphs``; the inner lists can be of variable length. Where there is no
|
|
chaining lookup at the given glyph position, the entry in ``lookups``
|
|
should be ``None``."""
|
|
|
|
def __init__(self, prefix, glyphs, suffix, lookups, location=None):
|
|
Statement.__init__(self, location)
|
|
self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix
|
|
self.lookups = lookups
|
|
|
|
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.glyph))
|
|
res += ";"
|
|
return res
|
|
|
|
|
|
class ChainContextSubstStatement(Statement):
|
|
"""A chained contextual substitution statement.
|
|
|
|
``prefix``, ``glyphs``, and ``suffix`` should be lists of
|
|
`glyph-containing objects`_ .
|
|
|
|
``lookups`` should be a list of :class:`LookupBlock` statements, with
|
|
length equal to the length of ``glyphs``. Where there is no chaining
|
|
lookup at the given glyph position, the entry in ``lookups`` should be
|
|
``None``."""
|
|
def __init__(self, prefix, glyphs, suffix, lookups, location=None):
|
|
Statement.__init__(self, location)
|
|
self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix
|
|
self.lookups = lookups
|
|
|
|
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.glyph))
|
|
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) 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 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 += " {} 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 = " <anchor NULL>"
|
|
else:
|
|
for a, m in l:
|
|
temp += " {} 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 += " {} mark @{}".format(a.asFea(), m.name)
|
|
res += ";"
|
|
return res
|
|
|
|
|
|
class MultipleSubstStatement(Statement):
|
|
"""A multiple substitution statement.
|
|
|
|
``prefix``, ``glyph``, ``suffix`` and ``replacement`` should be lists of
|
|
`glyph-containing objects`_.
|
|
|
|
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, 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]
|
|
builder.add_multiple_subst(
|
|
self.location, prefix, self.glyph, 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(map(asFea, self.prefix)) + " "
|
|
res += asFea(self.glyph) + "'"
|
|
if len(self.suffix):
|
|
res += " " + " ".join(map(asFea, self.suffix))
|
|
else:
|
|
res += asFea(self.glyph)
|
|
res += " by "
|
|
res += " ".join(map(asFea, self.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()]
|
|
for glyph1, glyph2 in itertools.product(*g):
|
|
builder.add_specific_pair_pos(
|
|
self.location, glyph1, self.valuerecord1,
|
|
glyph2, self.valuerecord2)
|
|
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] 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] 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 unichr(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 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, 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
|
|
|
|
def build(self, builder):
|
|
"""Calls the builder object's ``set_base_axis`` callback."""
|
|
builder.set_base_axis(self.bases, self.scripts, self.vertical)
|
|
|
|
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]
|
|
return "{}Axis.BaseTagList {};\n{}{}Axis.BaseScriptList {};".format(
|
|
direction, " ".join(self.bases), indent, direction, ", ".join(scripts))
|
|
|
|
|
|
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)
|