From ca8703f6534346f290e3b74b2f462bdbe9206e32 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Tue, 12 May 2020 23:11:17 +0100 Subject: [PATCH] [docs] Document feaLib (#1941) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [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. --- Doc/source/feaLib/ast.rst | 8 - Doc/source/feaLib/builder.rst | 8 - Doc/source/feaLib/error.rst | 8 - Doc/source/feaLib/index.rst | 49 +++-- Doc/source/feaLib/lexer.rst | 8 - Doc/source/feaLib/parser.rst | 8 - Lib/fontTools/feaLib/ast.py | 377 +++++++++++++++++++++++++++----- Lib/fontTools/feaLib/builder.py | 29 ++- Lib/fontTools/feaLib/parser.py | 75 +++++-- 9 files changed, 444 insertions(+), 126 deletions(-) delete mode 100644 Doc/source/feaLib/ast.rst delete mode 100644 Doc/source/feaLib/builder.rst delete mode 100644 Doc/source/feaLib/error.rst delete mode 100644 Doc/source/feaLib/lexer.rst delete mode 100644 Doc/source/feaLib/parser.rst diff --git a/Doc/source/feaLib/ast.rst b/Doc/source/feaLib/ast.rst deleted file mode 100644 index 6804299e8..000000000 --- a/Doc/source/feaLib/ast.rst +++ /dev/null @@ -1,8 +0,0 @@ -### -ast -### - -.. automodule:: fontTools.feaLib.ast - :inherited-members: - :members: - :undoc-members: diff --git a/Doc/source/feaLib/builder.rst b/Doc/source/feaLib/builder.rst deleted file mode 100644 index 8acbeada4..000000000 --- a/Doc/source/feaLib/builder.rst +++ /dev/null @@ -1,8 +0,0 @@ -####### -builder -####### - -.. automodule:: fontTools.feaLib.builder - :inherited-members: - :members: - :undoc-members: diff --git a/Doc/source/feaLib/error.rst b/Doc/source/feaLib/error.rst deleted file mode 100644 index fa85e0f59..000000000 --- a/Doc/source/feaLib/error.rst +++ /dev/null @@ -1,8 +0,0 @@ -##### -error -##### - -.. automodule:: fontTools.feaLib.error - :inherited-members: - :members: - :undoc-members: diff --git a/Doc/source/feaLib/index.rst b/Doc/source/feaLib/index.rst index 363761db8..61ac31f3b 100644 --- a/Doc/source/feaLib/index.rst +++ b/Doc/source/feaLib/index.rst @@ -1,17 +1,40 @@ -###### -feaLib -###### +######################################### +feaLib: Read/write OpenType feature files +######################################### -.. toctree:: - :maxdepth: 1 +fontTools' ``feaLib`` allows for the creation and parsing of Adobe +Font Development Kit for OpenType feature (``.fea``) files. The syntax +of these files is described `here `_. - ast - builder - error - lexer - parser +The :class:`fontTools.feaLib.parser.Parser` class can be used to parse files +into an abstract syntax tree, and from there the +:class:`fontTools.feaLib.builder.Builder` class can add features to an existing +font file. You can inspect the parsed syntax tree, walk the tree and do clever +things with it, and also generate your own feature files programmatically, by +using the classes in the :mod:`fontTools.feaLib.ast` module. -.. automodule:: fontTools.feaLib - :inherited-members: +Parsing +------- + +.. autoclass:: fontTools.feaLib.parser.Parser + :members: parse + :member-order: bysource + +Building +--------- + +.. automodule:: fontTools.feaLib.builder + :members: addOpenTypeFeatures, addOpenTypeFeaturesFromString + +Generation/Interrogation +------------------------ + +.. _`glyph-containing object`: +.. _`glyph-containing objects`: + +In the below, a **glyph-containing object** is an object of one of the following +classes: :class:`GlyphName`, :class:`GlyphClass`, :class:`GlyphClassName`. + +.. automodule:: fontTools.feaLib.ast + :member-order: bysource :members: - :undoc-members: diff --git a/Doc/source/feaLib/lexer.rst b/Doc/source/feaLib/lexer.rst deleted file mode 100644 index 939abf7ef..000000000 --- a/Doc/source/feaLib/lexer.rst +++ /dev/null @@ -1,8 +0,0 @@ -##### -lexer -##### - -.. automodule:: fontTools.feaLib.lexer - :inherited-members: - :members: - :undoc-members: diff --git a/Doc/source/feaLib/parser.rst b/Doc/source/feaLib/parser.rst deleted file mode 100644 index 2d5a61245..000000000 --- a/Doc/source/feaLib/parser.rst +++ /dev/null @@ -1,8 +0,0 @@ -###### -parser -###### - -.. automodule:: fontTools.feaLib.parser - :inherited-members: - :members: - :undoc-members: diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py index c630a514d..a305e67f7 100644 --- a/Lib/fontTools/feaLib/ast.py +++ b/Lib/fontTools/feaLib/ast.py @@ -7,33 +7,36 @@ import itertools SHIFT = " " * 4 __all__ = [ + 'Element', + 'FeatureFile', + 'Comment', + 'GlyphName', + 'GlyphClass', + 'GlyphClassName', + 'MarkClassName', + 'AnonymousBlock', + 'Block', + 'FeatureBlock', + 'NestedBlock', + 'LookupBlock', + 'GlyphClassDefinition', + 'GlyphClassDefStatement', + 'MarkClass', + 'MarkClassDefinition', 'AlternateSubstStatement', 'Anchor', 'AnchorDefinition', - 'AnonymousBlock', 'AttachStatement', 'BaseAxis', - 'Block', - 'BytesIO', 'CVParametersNameStatement', 'ChainContextPosStatement', 'ChainContextSubstStatement', 'CharacterStatement', - 'Comment', 'CursivePosStatement', - 'Element', 'Expression', - 'FeatureBlock', - 'FeatureFile', - 'FeatureLibError', 'FeatureNameStatement', 'FeatureReferenceStatement', 'FontRevisionStatement', - 'GlyphClass', - 'GlyphClassDefStatement', - 'GlyphClassDefinition', - 'GlyphClassName', - 'GlyphName', 'HheaField', 'IgnorePosStatement', 'IgnoreSubstStatement', @@ -43,34 +46,23 @@ __all__ = [ 'LigatureCaretByIndexStatement', 'LigatureCaretByPosStatement', 'LigatureSubstStatement', - 'LookupBlock', 'LookupFlagStatement', 'LookupReferenceStatement', 'MarkBasePosStatement', - 'MarkClass', - 'MarkClassDefinition', - 'MarkClassName', 'MarkLigPosStatement', 'MarkMarkPosStatement', 'MultipleSubstStatement', 'NameRecord', - 'NestedBlock', 'OS2Field', - 'OrderedDict', 'PairPosStatement', - 'Py23Error', 'ReverseChainSingleSubstStatement', 'ScriptStatement', - 'SimpleNamespace', 'SinglePosStatement', 'SingleSubstStatement', 'SizeParameters', 'Statement', - 'StringIO', 'SubtableStatement', 'TableBlock', - 'Tag', - 'UnicodeIO', 'ValueRecord', 'ValueRecordDefinition', 'VheaField', @@ -117,14 +109,19 @@ def asFea(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): @@ -140,8 +137,10 @@ class Expression(Element): 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=""): @@ -149,12 +148,14 @@ class Comment(Element): class GlyphName(Expression): - """A single glyph name, such as cedilla.""" + """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=""): @@ -162,14 +163,16 @@ class GlyphName(Expression): class GlyphClass(Expression): - """A glyph class, such as [acute cedilla grave].""" + """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=""): @@ -182,12 +185,18 @@ class GlyphClass(Expression): 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)) @@ -195,6 +204,9 @@ class GlyphClass(Expression): 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))) @@ -202,6 +214,8 @@ class GlyphClass(Expression): 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) @@ -210,13 +224,15 @@ class GlyphClass(Expression): class GlyphClassName(Expression): - """A glyph class name, such as @FRENCH_MARKS.""" + """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=""): @@ -224,13 +240,15 @@ class GlyphClassName(Expression): class MarkClassName(Expression): - """A mark class name, such as @FRENCH_MARKS defined with markClass.""" + """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=""): @@ -238,9 +256,12 @@ class MarkClassName(Expression): class AnonymousBlock(Statement): + """An anonymous data block.""" + def __init__(self, tag, content, location=None): Statement.__init__(self, location) - self.tag, self.content = tag, content + 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) @@ -250,11 +271,15 @@ class AnonymousBlock(Statement): class Block(Statement): + """A block of statements: feature, lookup, etc.""" def __init__(self, location=None): Statement.__init__(self, location) - self.statements = [] + 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) @@ -265,6 +290,8 @@ class Block(Statement): 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 @@ -274,11 +301,14 @@ class FeatureFile(Block): 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_ @@ -302,6 +332,8 @@ class FeatureBlock(Block): 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 @@ -320,6 +352,7 @@ class NestedBlock(Block): 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 @@ -341,6 +374,7 @@ class LookupBlock(Block): class TableBlock(Block): + """A ``table ... { }`` block.""" def __init__(self, name, location=None): Block.__init__(self, location) self.name = name @@ -353,13 +387,14 @@ class TableBlock(Block): class GlyphClassDefinition(Statement): - """Example: @UPPERCASE = [A-Z];""" + """Example: ``@UPPERCASE = [A-Z];``.""" def __init__(self, name, glyphs, location=None): Statement.__init__(self, location) - self.name = name - self.glyphs = glyphs + 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=""): @@ -367,7 +402,9 @@ class GlyphClassDefinition(Statement): class GlyphClassDefStatement(Statement): - """Example: GlyphClassDef @UPPERCASE, [B], [C], [D];""" + """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) @@ -376,6 +413,7 @@ class GlyphClassDefStatement(Statement): 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() @@ -392,19 +430,28 @@ class GlyphClassDefStatement(Statement): self.componentGlyphs.asFea() if self.componentGlyphs else "") -# 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] @FRENCH_ACCENTS; -# markClass [cedilla] @FRENCH_ACCENTS; 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] @FRENCH_ACCENTS; + markClass [cedilla] @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(): @@ -421,6 +468,7 @@ class MarkClass(object): 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=""): @@ -429,6 +477,27 @@ class MarkClass(object): 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] @FRENCH_ACCENTS; + # markClass [cedilla] @FRENCH_ACCENTS; + + """ def __init__(self, markClass, anchor, glyphs, location=None): Statement.__init__(self, location) assert isinstance(markClass, MarkClass) @@ -436,6 +505,7 @@ class MarkClassDefinition(Statement): 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=""): @@ -445,12 +515,18 @@ class MarkClassDefinition(Statement): 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] @@ -477,6 +553,11 @@ class AlternateSubstStatement(Statement): 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) @@ -500,6 +581,7 @@ class Anchor(Expression): 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 @@ -513,11 +595,14 @@ class AnchorDefinition(Statement): class AttachStatement(Statement): + """A ``GDEF`` table ``Attach`` statement.""" def __init__(self, glyphs, contourPoints, location=None): Statement.__init__(self, location) - self.glyphs, self.contourPoints = (glyphs, contourPoints) + 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) @@ -527,12 +612,24 @@ class AttachStatement(Statement): 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] @@ -560,12 +657,22 @@ class ChainContextPosStatement(Statement): 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] @@ -593,12 +700,15 @@ class ChainContextSubstStatement(Statement): 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) @@ -609,12 +719,13 @@ class CursivePosStatement(Statement): class FeatureReferenceStatement(Statement): - """Example: feature salt;""" + """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=""): @@ -622,11 +733,19 @@ class FeatureReferenceStatement(Statement): 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] @@ -651,11 +770,18 @@ class IgnorePosStatement(Statement): 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] @@ -680,9 +806,10 @@ class IgnoreSubstStatement(Statement): class IncludeStatement(Statement): + """An ``include()`` statement.""" def __init__(self, filename, location=None): super(IncludeStatement, self).__init__(location) - self.filename = filename + self.filename = filename #: String containing name of file to include def build(self): # TODO: consider lazy-loading the including parser/lexer? @@ -696,15 +823,17 @@ class IncludeStatement(Statement): 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 - self.include_default = include_default + 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) @@ -720,11 +849,13 @@ class LanguageStatement(Statement): 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=""): @@ -732,6 +863,8 @@ class LanguageSystemStatement(Statement): 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 @@ -744,11 +877,14 @@ class FontRevisionStatement(Statement): 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)) @@ -758,11 +894,14 @@ class LigatureCaretByIndexStatement(Statement): 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)) @@ -772,6 +911,14 @@ class LigatureCaretByPosStatement(Statement): 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) @@ -803,6 +950,10 @@ class LigatureSubstStatement(Statement): 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) @@ -811,6 +962,7 @@ class LookupFlagStatement(Statement): 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() @@ -838,11 +990,15 @@ class LookupFlagStatement(Statement): 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=""): @@ -850,11 +1006,15 @@ class LookupReferenceStatement(Statement): 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=""): @@ -866,11 +1026,38 @@ class MarkBasePosStatement(Statement): 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 mark @TOP_MARKS + # ligComponent 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=""): @@ -890,11 +1077,15 @@ class MarkLigPosStatement(Statement): 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=""): @@ -906,6 +1097,13 @@ class MarkMarkPosStatement(Statement): 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 ): @@ -915,6 +1113,7 @@ class MultipleSubstStatement(Statement): 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( @@ -938,6 +1137,14 @@ class MultipleSubstStatement(Statement): 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 `_. + """ def __init__(self, glyphs1, valuerecord1, glyphs2, valuerecord2, enumerated=False, location=None): Statement.__init__(self, location) @@ -946,6 +1153,14 @@ class PairPosStatement(Statement): 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): @@ -979,6 +1194,13 @@ class PairPosStatement(Statement): 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) @@ -1011,6 +1233,14 @@ class ReverseChainSingleSubstStatement(Statement): 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) @@ -1020,6 +1250,7 @@ class SingleSubstStatement(Statement): 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() @@ -1045,11 +1276,13 @@ class SingleSubstStatement(Statement): class ScriptStatement(Statement): + """A ``script`` statement.""" def __init__(self, script, location=None): Statement.__init__(self, location) - self.script = script + 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=""): @@ -1057,12 +1290,19 @@ class ScriptStatement(Statement): 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] @@ -1086,10 +1326,12 @@ class SinglePosStatement(Statement): 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=""): @@ -1097,6 +1339,7 @@ class SubtableStatement(Statement): class ValueRecord(Expression): + """Represents a value record.""" def __init__(self, xPlacement=None, yPlacement=None, xAdvance=None, yAdvance=None, xPlaDevice=None, yPlaDevice=None, @@ -1179,10 +1422,11 @@ class ValueRecord(Expression): class ValueRecordDefinition(Statement): + """Represents a named value record definition.""" def __init__(self, name, value, location=None): Statement.__init__(self, location) - self.name = name - self.value = value + 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) @@ -1198,16 +1442,18 @@ def simplify_name_attributes(pid, eid, lid): class NameRecord(Statement): + """Represents a name record. (`Section 9.e. `_)""" def __init__(self, nameID, platformID, platEncID, langID, string, location=None): Statement.__init__(self, location) - self.nameID = nameID - self.platformID = platformID - self.platEncID = platEncID - self.langID = langID - self.string = string + 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) @@ -1237,7 +1483,10 @@ class NameRecord(Statement): 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) @@ -1253,6 +1502,7 @@ class FeatureNameStatement(NameRecord): class SizeParameters(Statement): + """A ``parameters`` statement.""" def __init__(self, DesignSize, SubfamilyID, RangeStart, RangeEnd, location=None): Statement.__init__(self, location) @@ -1262,6 +1512,7 @@ class SizeParameters(Statement): 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) @@ -1273,6 +1524,7 @@ class SizeParameters(Statement): 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, @@ -1280,6 +1532,7 @@ class CVParametersNameStatement(NameRecord): 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)) @@ -1308,6 +1561,7 @@ class CharacterStatement(Statement): 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=""): @@ -1315,13 +1569,16 @@ class CharacterStatement(Statement): 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 - self.scripts = scripts - self.vertical = vertical + 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=""): @@ -1332,12 +1589,16 @@ class BaseAxis(Statement): 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=""): @@ -1357,12 +1618,14 @@ class OS2Field(Statement): 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=""): @@ -1372,12 +1635,14 @@ class HheaField(Statement): 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=""): diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py index d8c4476ce..c1dc920e3 100644 --- a/Lib/fontTools/feaLib/builder.py +++ b/Lib/fontTools/feaLib/builder.py @@ -17,15 +17,38 @@ log = logging.getLogger(__name__) def addOpenTypeFeatures(font, featurefile, tables=None): + """Add features from a file to a font. Note that this replaces any features + currently present. + + Args: + font (feaLib.ttLib.TTFont): The font object. + featurefile: Either a path or file object (in which case we + parse it into an AST), or a pre-parsed AST instance. + tables: If passed, restrict the set of affected tables to those in the + list. + + """ builder = Builder(font, featurefile) builder.build(tables=tables) def addOpenTypeFeaturesFromString(font, features, filename=None, tables=None): + """Add features from a string to a font. Note that this replaces any + features currently present. + + Args: + font (feaLib.ttLib.TTFont): The font object. + features: A string containing feature code. + filename: The directory containing ``filename`` is used as the root of + relative ``include()`` paths; if ``None`` is provided, the current + directory is assumed. + tables: If passed, restrict the set of affected tables to those in the + list. + + """ + featurefile = UnicodeIO(tounicode(features)) if filename: - # the directory containing 'filename' is used as the root of relative - # include paths; if None is provided, the current directory is assumed featurefile.name = filename addOpenTypeFeatures(font, featurefile, tables=tables) @@ -855,7 +878,7 @@ class Builder(object): self.cv_parameters_.add(tag) def add_to_cv_num_named_params(self, tag): - """Adds new items to self.cv_num_named_params_ + """Adds new items to ``self.cv_num_named_params_`` or increments the count of existing items.""" if tag in self.cv_num_named_params_: self.cv_num_named_params_[tag] += 1 diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py index 6f42fbe4d..3a63c6e0c 100644 --- a/Lib/fontTools/feaLib/parser.py +++ b/Lib/fontTools/feaLib/parser.py @@ -12,6 +12,27 @@ log = logging.getLogger(__name__) class Parser(object): + """Initializes a Parser object. + + Example: + + .. code:: python + + from fontTools.feaLib.parser import Parser + parser = Parser(file, font.getReverseGlyphMap()) + parsetree = parser.parse() + + Note: the ``glyphNames`` iterable serves a double role to help distinguish + glyph names from ranges in the presence of hyphens and to ensure that glyph + names referenced in a feature file are actually part of a font's glyph set. + If the iterable is left empty, no glyph name in glyph set checking takes + place, and all glyph tokens containing hyphens are treated as literal glyph + names, not as ranges. (Adding a space around the hyphen can, in any case, + help to disambiguate ranges from glyph names containing hyphens.) + + By default, the parser will follow ``include()`` statements in the feature + file. To turn this off, pass ``followIncludes=False``. + """ extensions = {} ast = ast SS_FEATURE_TAGS = {"ss%02d" % i for i in range(1, 20+1)} @@ -19,14 +40,7 @@ class Parser(object): def __init__(self, featurefile, glyphNames=(), followIncludes=True, **kwargs): - """Initializes a Parser object. - Note: the `glyphNames` iterable serves a double role to help distinguish - glyph names from ranges in the presence of hyphens and to ensure that glyph - names referenced in a feature file are actually part of a font's glyph set. - If the iterable is left empty, no glyph name in glyph set checking takes - place. - """ if "glyphMap" in kwargs: from fontTools.misc.loggingTools import deprecateArgument deprecateArgument("glyphMap", "use 'glyphNames' (iterable) instead") @@ -56,6 +70,9 @@ class Parser(object): self.advance_lexer_(comments=True) def parse(self): + """Parse the file, and return a :class:`fontTools.feaLib.ast.FeatureFile` + object representing the root of the abstract syntax tree containing the + parsed contents of the file.""" statements = self.doc_.statements while self.next_token_type_ is not None or self.cur_comments_: self.advance_lexer_(comments=True) @@ -96,16 +113,18 @@ class Parser(object): return self.doc_ def parse_anchor_(self): + # Parses an anchor in any of the four formats given in the feature + # file specification (2.e.vii). self.expect_symbol_("<") self.expect_keyword_("anchor") location = self.cur_token_location_ - if self.next_token_ == "NULL": + if self.next_token_ == "NULL": # Format D self.expect_keyword_("NULL") self.expect_symbol_(">") return None - if self.next_token_type_ == Lexer.NAME: + if self.next_token_type_ == Lexer.NAME: # Format E name = self.expect_name_() anchordef = self.anchors_.resolve(name) if anchordef is None: @@ -122,11 +141,11 @@ class Parser(object): x, y = self.expect_number_(), self.expect_number_() contourpoint = None - if self.next_token_ == "contourpoint": + if self.next_token_ == "contourpoint": # Format B self.expect_keyword_("contourpoint") contourpoint = self.expect_number_() - if self.next_token_ == "<": + if self.next_token_ == "<": # Format C xDeviceTable = self.parse_device_() yDeviceTable = self.parse_device_() else: @@ -140,7 +159,7 @@ class Parser(object): location=location) def parse_anchor_marks_(self): - """Parses a sequence of [ mark @MARKCLASS]*.""" + # Parses a sequence of ``[ mark @MARKCLASS]*.`` anchorMarks = [] # [(self.ast.Anchor, markClassName)*] while self.next_token_ == "<": anchor = self.parse_anchor_() @@ -152,6 +171,7 @@ class Parser(object): return anchorMarks def parse_anchordef_(self): + # Parses a named anchor definition (`section 2.e.viii `_). assert self.is_cur_keyword_("anchorDef") location = self.cur_token_location_ x, y = self.expect_number_(), self.expect_number_() @@ -168,6 +188,7 @@ class Parser(object): return anchordef def parse_anonymous_(self): + # Parses an anonymous data block (`section 10 `_). assert self.is_cur_keyword_(("anon", "anonymous")) tag = self.expect_tag_() _, content, location = self.lexer_.scan_anonymous_block(tag) @@ -179,6 +200,7 @@ class Parser(object): return self.ast.AnonymousBlock(tag, content, location=location) def parse_attach_(self): + # Parses a GDEF Attach statement (`section 9.b `_) assert self.is_cur_keyword_("Attach") location = self.cur_token_location_ glyphs = self.parse_glyphclass_(accept_glyphname=True) @@ -190,12 +212,13 @@ class Parser(object): location=location) def parse_enumerate_(self, vertical): + # Parse an enumerated pair positioning rule (`section 6.b.ii `_). assert self.cur_token_ in {"enumerate", "enum"} self.advance_lexer_() return self.parse_position_(enumerated=True, vertical=vertical) def parse_GlyphClassDef_(self): - """Parses 'GlyphClassDef @BASE, @LIGATURES, @MARKS, @COMPONENTS;'""" + # Parses 'GlyphClassDef @BASE, @LIGATURES, @MARKS, @COMPONENTS;' assert self.is_cur_keyword_("GlyphClassDef") location = self.cur_token_location_ if self.next_token_ != ",": @@ -223,7 +246,7 @@ class Parser(object): location=location) def parse_glyphclass_definition_(self): - """Parses glyph class definitions such as '@UPPERCASE = [A-Z];'""" + # Parses glyph class definitions such as '@UPPERCASE = [A-Z];' location, name = self.cur_token_location_, self.cur_token_ self.expect_symbol_("=") glyphs = self.parse_glyphclass_(accept_glyphname=False) @@ -273,6 +296,8 @@ class Parser(object): location) def parse_glyphclass_(self, accept_glyphname): + # Parses a glyph class, either named or anonymous, or (if + # ``bool(accept_glyphname)``) a glyph name. if (accept_glyphname and self.next_token_type_ in (Lexer.NAME, Lexer.CID)): glyph = self.expect_glyph_() @@ -362,6 +387,7 @@ class Parser(object): return glyphs def parse_class_name_(self): + # Parses named class - either a glyph class or mark class. name = self.expect_class_name_() gc = self.glyphclasses_.resolve(name) if gc is None: @@ -376,6 +402,11 @@ class Parser(object): gc, location=self.cur_token_location_) def parse_glyph_pattern_(self, vertical): + # Parses a glyph pattern, including lookups and context, e.g.:: + # + # a b + # a b c' d e + # a b c' lookup ChangeC d e prefix, glyphs, lookups, values, suffix = ([], [], [], [], []) hasMarks = False while self.next_token_ not in {"by", "from", ";", ","}: @@ -449,6 +480,7 @@ class Parser(object): return chainContext, hasLookups def parse_ignore_(self): + # Parses an ignore sub/pos rule. assert self.is_cur_keyword_("ignore") location = self.cur_token_location_ self.advance_lexer_() @@ -517,6 +549,8 @@ class Parser(object): location=location) def parse_lookup_(self, vertical): + # Parses a ``lookup`` - either a lookup block, or a lookup reference + # inside a feature. assert self.is_cur_keyword_("lookup") location, name = self.cur_token_location_, self.expect_name_() @@ -540,6 +574,8 @@ class Parser(object): return block def parse_lookupflag_(self): + # Parses a ``lookupflag`` statement, either specified by number or + # in words. assert self.is_cur_keyword_("lookupflag") location = self.cur_token_location_ @@ -853,6 +889,8 @@ class Parser(object): return self.ast.SubtableStatement(location=location) def parse_size_parameters_(self): + # Parses a ``parameters`` statement used in ``size`` features. See + # `section 8.b `_. assert self.is_cur_keyword_("parameters") location = self.cur_token_location_ DesignSize = self.expect_decipoint_() @@ -1006,6 +1044,7 @@ class Parser(object): self.cur_token_location_) def parse_name_(self): + """Parses a name record. See `section 9.e `_.""" platEncID = None langID = None if self.next_token_type_ in Lexer.NUMBERS: @@ -1133,6 +1172,7 @@ class Parser(object): continue def parse_base_tag_list_(self): + # Parses BASE table entries. (See `section 9.a `_) assert self.cur_token_ in ("HorizAxis.BaseTagList", "VertAxis.BaseTagList"), self.cur_token_ bases = [] @@ -1232,6 +1272,7 @@ class Parser(object): vertical=vertical, location=location) def parse_valuerecord_definition_(self, vertical): + # Parses a named value record definition. (See section `2.e.v `_) assert self.is_cur_keyword_("valueRecordDef") location = self.cur_token_location_ value = self.parse_valuerecord_(vertical) @@ -1286,6 +1327,8 @@ class Parser(object): location=location) def parse_featureNames_(self, tag): + """Parses a ``featureNames`` statement found in stylistic set features. + See section `8.c `_.""" assert self.cur_token_ == "featureNames", self.cur_token_ block = self.ast.NestedBlock(tag, self.cur_token_, location=self.cur_token_location_) @@ -1316,6 +1359,8 @@ class Parser(object): return block def parse_cvParameters_(self, tag): + # Parses a ``cvParameters`` block found in Character Variant features. + # See section `8.d `_. assert self.cur_token_ == "cvParameters", self.cur_token_ block = self.ast.NestedBlock(tag, self.cur_token_, location=self.cur_token_location_) @@ -1391,6 +1436,8 @@ class Parser(object): return self.ast.CharacterStatement(character, tag, location=location) def parse_FontRevision_(self): + # Parses a ``FontRevision`` statement found in the head table. See + # `section 9.c `_. assert self.cur_token_ == "FontRevision", self.cur_token_ location, version = self.cur_token_location_, self.expect_float_() self.expect_symbol_(";")