[docs] Document feaLib (#1941)

[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.
This commit is contained in:
Simon Cozens 2020-05-12 23:11:17 +01:00 committed by GitHub
parent 089f24da6b
commit ca8703f653
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 444 additions and 126 deletions

View File

@ -1,8 +0,0 @@
###
ast
###
.. automodule:: fontTools.feaLib.ast
:inherited-members:
:members:
:undoc-members:

View File

@ -1,8 +0,0 @@
#######
builder
#######
.. automodule:: fontTools.feaLib.builder
:inherited-members:
:members:
:undoc-members:

View File

@ -1,8 +0,0 @@
#####
error
#####
.. automodule:: fontTools.feaLib.error
:inherited-members:
:members:
:undoc-members:

View File

@ -1,17 +1,40 @@
###### #########################################
feaLib feaLib: Read/write OpenType feature files
###### #########################################
.. toctree:: fontTools' ``feaLib`` allows for the creation and parsing of Adobe
:maxdepth: 1 Font Development Kit for OpenType feature (``.fea``) files. The syntax
of these files is described `here <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html>`_.
ast The :class:`fontTools.feaLib.parser.Parser` class can be used to parse files
builder into an abstract syntax tree, and from there the
error :class:`fontTools.feaLib.builder.Builder` class can add features to an existing
lexer font file. You can inspect the parsed syntax tree, walk the tree and do clever
parser 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 Parsing
:inherited-members: -------
.. 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: :members:
:undoc-members:

View File

@ -1,8 +0,0 @@
#####
lexer
#####
.. automodule:: fontTools.feaLib.lexer
:inherited-members:
:members:
:undoc-members:

View File

@ -1,8 +0,0 @@
######
parser
######
.. automodule:: fontTools.feaLib.parser
:inherited-members:
:members:
:undoc-members:

File diff suppressed because it is too large Load Diff

View File

@ -17,15 +17,38 @@ log = logging.getLogger(__name__)
def addOpenTypeFeatures(font, featurefile, tables=None): 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 = Builder(font, featurefile)
builder.build(tables=tables) builder.build(tables=tables)
def addOpenTypeFeaturesFromString(font, features, filename=None, tables=None): 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)) featurefile = UnicodeIO(tounicode(features))
if filename: 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 featurefile.name = filename
addOpenTypeFeatures(font, featurefile, tables=tables) addOpenTypeFeatures(font, featurefile, tables=tables)
@ -855,7 +878,7 @@ class Builder(object):
self.cv_parameters_.add(tag) self.cv_parameters_.add(tag)
def add_to_cv_num_named_params(self, 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.""" or increments the count of existing items."""
if tag in self.cv_num_named_params_: if tag in self.cv_num_named_params_:
self.cv_num_named_params_[tag] += 1 self.cv_num_named_params_[tag] += 1

View File

@ -12,6 +12,27 @@ log = logging.getLogger(__name__)
class Parser(object): 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 = {} extensions = {}
ast = ast ast = ast
SS_FEATURE_TAGS = {"ss%02d" % i for i in range(1, 20+1)} 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, def __init__(self, featurefile, glyphNames=(), followIncludes=True,
**kwargs): **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: if "glyphMap" in kwargs:
from fontTools.misc.loggingTools import deprecateArgument from fontTools.misc.loggingTools import deprecateArgument
deprecateArgument("glyphMap", "use 'glyphNames' (iterable) instead") deprecateArgument("glyphMap", "use 'glyphNames' (iterable) instead")
@ -56,6 +70,9 @@ class Parser(object):
self.advance_lexer_(comments=True) self.advance_lexer_(comments=True)
def parse(self): 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 statements = self.doc_.statements
while self.next_token_type_ is not None or self.cur_comments_: while self.next_token_type_ is not None or self.cur_comments_:
self.advance_lexer_(comments=True) self.advance_lexer_(comments=True)
@ -96,16 +113,18 @@ class Parser(object):
return self.doc_ return self.doc_
def parse_anchor_(self): 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_symbol_("<")
self.expect_keyword_("anchor") self.expect_keyword_("anchor")
location = self.cur_token_location_ location = self.cur_token_location_
if self.next_token_ == "NULL": if self.next_token_ == "NULL": # Format D
self.expect_keyword_("NULL") self.expect_keyword_("NULL")
self.expect_symbol_(">") self.expect_symbol_(">")
return None return None
if self.next_token_type_ == Lexer.NAME: if self.next_token_type_ == Lexer.NAME: # Format E
name = self.expect_name_() name = self.expect_name_()
anchordef = self.anchors_.resolve(name) anchordef = self.anchors_.resolve(name)
if anchordef is None: if anchordef is None:
@ -122,11 +141,11 @@ class Parser(object):
x, y = self.expect_number_(), self.expect_number_() x, y = self.expect_number_(), self.expect_number_()
contourpoint = None contourpoint = None
if self.next_token_ == "contourpoint": if self.next_token_ == "contourpoint": # Format B
self.expect_keyword_("contourpoint") self.expect_keyword_("contourpoint")
contourpoint = self.expect_number_() contourpoint = self.expect_number_()
if self.next_token_ == "<": if self.next_token_ == "<": # Format C
xDeviceTable = self.parse_device_() xDeviceTable = self.parse_device_()
yDeviceTable = self.parse_device_() yDeviceTable = self.parse_device_()
else: else:
@ -140,7 +159,7 @@ class Parser(object):
location=location) location=location)
def parse_anchor_marks_(self): def parse_anchor_marks_(self):
"""Parses a sequence of [<anchor> mark @MARKCLASS]*.""" # Parses a sequence of ``[<anchor> mark @MARKCLASS]*.``
anchorMarks = [] # [(self.ast.Anchor, markClassName)*] anchorMarks = [] # [(self.ast.Anchor, markClassName)*]
while self.next_token_ == "<": while self.next_token_ == "<":
anchor = self.parse_anchor_() anchor = self.parse_anchor_()
@ -152,6 +171,7 @@ class Parser(object):
return anchorMarks return anchorMarks
def parse_anchordef_(self): def parse_anchordef_(self):
# Parses a named anchor definition (`section 2.e.viii <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#2.e.vii>`_).
assert self.is_cur_keyword_("anchorDef") assert self.is_cur_keyword_("anchorDef")
location = self.cur_token_location_ location = self.cur_token_location_
x, y = self.expect_number_(), self.expect_number_() x, y = self.expect_number_(), self.expect_number_()
@ -168,6 +188,7 @@ class Parser(object):
return anchordef return anchordef
def parse_anonymous_(self): def parse_anonymous_(self):
# Parses an anonymous data block (`section 10 <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#10>`_).
assert self.is_cur_keyword_(("anon", "anonymous")) assert self.is_cur_keyword_(("anon", "anonymous"))
tag = self.expect_tag_() tag = self.expect_tag_()
_, content, location = self.lexer_.scan_anonymous_block(tag) _, content, location = self.lexer_.scan_anonymous_block(tag)
@ -179,6 +200,7 @@ class Parser(object):
return self.ast.AnonymousBlock(tag, content, location=location) return self.ast.AnonymousBlock(tag, content, location=location)
def parse_attach_(self): def parse_attach_(self):
# Parses a GDEF Attach statement (`section 9.b <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.b>`_)
assert self.is_cur_keyword_("Attach") assert self.is_cur_keyword_("Attach")
location = self.cur_token_location_ location = self.cur_token_location_
glyphs = self.parse_glyphclass_(accept_glyphname=True) glyphs = self.parse_glyphclass_(accept_glyphname=True)
@ -190,12 +212,13 @@ class Parser(object):
location=location) location=location)
def parse_enumerate_(self, vertical): def parse_enumerate_(self, vertical):
# Parse an enumerated pair positioning rule (`section 6.b.ii <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#6.b.ii>`_).
assert self.cur_token_ in {"enumerate", "enum"} assert self.cur_token_ in {"enumerate", "enum"}
self.advance_lexer_() self.advance_lexer_()
return self.parse_position_(enumerated=True, vertical=vertical) return self.parse_position_(enumerated=True, vertical=vertical)
def parse_GlyphClassDef_(self): def parse_GlyphClassDef_(self):
"""Parses 'GlyphClassDef @BASE, @LIGATURES, @MARKS, @COMPONENTS;'""" # Parses 'GlyphClassDef @BASE, @LIGATURES, @MARKS, @COMPONENTS;'
assert self.is_cur_keyword_("GlyphClassDef") assert self.is_cur_keyword_("GlyphClassDef")
location = self.cur_token_location_ location = self.cur_token_location_
if self.next_token_ != ",": if self.next_token_ != ",":
@ -223,7 +246,7 @@ class Parser(object):
location=location) location=location)
def parse_glyphclass_definition_(self): 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_ location, name = self.cur_token_location_, self.cur_token_
self.expect_symbol_("=") self.expect_symbol_("=")
glyphs = self.parse_glyphclass_(accept_glyphname=False) glyphs = self.parse_glyphclass_(accept_glyphname=False)
@ -273,6 +296,8 @@ class Parser(object):
location) location)
def parse_glyphclass_(self, accept_glyphname): 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 if (accept_glyphname and
self.next_token_type_ in (Lexer.NAME, Lexer.CID)): self.next_token_type_ in (Lexer.NAME, Lexer.CID)):
glyph = self.expect_glyph_() glyph = self.expect_glyph_()
@ -362,6 +387,7 @@ class Parser(object):
return glyphs return glyphs
def parse_class_name_(self): def parse_class_name_(self):
# Parses named class - either a glyph class or mark class.
name = self.expect_class_name_() name = self.expect_class_name_()
gc = self.glyphclasses_.resolve(name) gc = self.glyphclasses_.resolve(name)
if gc is None: if gc is None:
@ -376,6 +402,11 @@ class Parser(object):
gc, location=self.cur_token_location_) gc, location=self.cur_token_location_)
def parse_glyph_pattern_(self, vertical): 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 = ([], [], [], [], []) prefix, glyphs, lookups, values, suffix = ([], [], [], [], [])
hasMarks = False hasMarks = False
while self.next_token_ not in {"by", "from", ";", ","}: while self.next_token_ not in {"by", "from", ";", ","}:
@ -449,6 +480,7 @@ class Parser(object):
return chainContext, hasLookups return chainContext, hasLookups
def parse_ignore_(self): def parse_ignore_(self):
# Parses an ignore sub/pos rule.
assert self.is_cur_keyword_("ignore") assert self.is_cur_keyword_("ignore")
location = self.cur_token_location_ location = self.cur_token_location_
self.advance_lexer_() self.advance_lexer_()
@ -517,6 +549,8 @@ class Parser(object):
location=location) location=location)
def parse_lookup_(self, vertical): 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") assert self.is_cur_keyword_("lookup")
location, name = self.cur_token_location_, self.expect_name_() location, name = self.cur_token_location_, self.expect_name_()
@ -540,6 +574,8 @@ class Parser(object):
return block return block
def parse_lookupflag_(self): def parse_lookupflag_(self):
# Parses a ``lookupflag`` statement, either specified by number or
# in words.
assert self.is_cur_keyword_("lookupflag") assert self.is_cur_keyword_("lookupflag")
location = self.cur_token_location_ location = self.cur_token_location_
@ -853,6 +889,8 @@ class Parser(object):
return self.ast.SubtableStatement(location=location) return self.ast.SubtableStatement(location=location)
def parse_size_parameters_(self): def parse_size_parameters_(self):
# Parses a ``parameters`` statement used in ``size`` features. See
# `section 8.b <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.b>`_.
assert self.is_cur_keyword_("parameters") assert self.is_cur_keyword_("parameters")
location = self.cur_token_location_ location = self.cur_token_location_
DesignSize = self.expect_decipoint_() DesignSize = self.expect_decipoint_()
@ -1006,6 +1044,7 @@ class Parser(object):
self.cur_token_location_) self.cur_token_location_)
def parse_name_(self): def parse_name_(self):
"""Parses a name record. See `section 9.e <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.e>`_."""
platEncID = None platEncID = None
langID = None langID = None
if self.next_token_type_ in Lexer.NUMBERS: if self.next_token_type_ in Lexer.NUMBERS:
@ -1133,6 +1172,7 @@ class Parser(object):
continue continue
def parse_base_tag_list_(self): def parse_base_tag_list_(self):
# Parses BASE table entries. (See `section 9.a <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.a>`_)
assert self.cur_token_ in ("HorizAxis.BaseTagList", assert self.cur_token_ in ("HorizAxis.BaseTagList",
"VertAxis.BaseTagList"), self.cur_token_ "VertAxis.BaseTagList"), self.cur_token_
bases = [] bases = []
@ -1232,6 +1272,7 @@ class Parser(object):
vertical=vertical, location=location) vertical=vertical, location=location)
def parse_valuerecord_definition_(self, vertical): def parse_valuerecord_definition_(self, vertical):
# Parses a named value record definition. (See section `2.e.v <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#2.e.v>`_)
assert self.is_cur_keyword_("valueRecordDef") assert self.is_cur_keyword_("valueRecordDef")
location = self.cur_token_location_ location = self.cur_token_location_
value = self.parse_valuerecord_(vertical) value = self.parse_valuerecord_(vertical)
@ -1286,6 +1327,8 @@ class Parser(object):
location=location) location=location)
def parse_featureNames_(self, tag): def parse_featureNames_(self, tag):
"""Parses a ``featureNames`` statement found in stylistic set features.
See section `8.c <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.c>`_."""
assert self.cur_token_ == "featureNames", self.cur_token_ assert self.cur_token_ == "featureNames", self.cur_token_
block = self.ast.NestedBlock(tag, self.cur_token_, block = self.ast.NestedBlock(tag, self.cur_token_,
location=self.cur_token_location_) location=self.cur_token_location_)
@ -1316,6 +1359,8 @@ class Parser(object):
return block return block
def parse_cvParameters_(self, tag): def parse_cvParameters_(self, tag):
# Parses a ``cvParameters`` block found in Character Variant features.
# See section `8.d <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.d>`_.
assert self.cur_token_ == "cvParameters", self.cur_token_ assert self.cur_token_ == "cvParameters", self.cur_token_
block = self.ast.NestedBlock(tag, self.cur_token_, block = self.ast.NestedBlock(tag, self.cur_token_,
location=self.cur_token_location_) location=self.cur_token_location_)
@ -1391,6 +1436,8 @@ class Parser(object):
return self.ast.CharacterStatement(character, tag, location=location) return self.ast.CharacterStatement(character, tag, location=location)
def parse_FontRevision_(self): def parse_FontRevision_(self):
# Parses a ``FontRevision`` statement found in the head table. See
# `section 9.c <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.c>`_.
assert self.cur_token_ == "FontRevision", self.cur_token_ assert self.cur_token_ == "FontRevision", self.cur_token_
location, version = self.cur_token_location_, self.expect_float_() location, version = self.cur_token_location_, self.expect_float_()
self.expect_symbol_(";") self.expect_symbol_(";")