2360 lines
96 KiB
Python
2360 lines
96 KiB
Python
|
from fontTools.feaLib.error import FeatureLibError
|
||
|
from fontTools.feaLib.lexer import Lexer, IncludingLexer, NonIncludingLexer
|
||
|
from fontTools.feaLib.variableScalar import VariableScalar
|
||
|
from fontTools.misc.encodingTools import getEncoding
|
||
|
from fontTools.misc.textTools import bytechr, tobytes, tostr
|
||
|
import fontTools.feaLib.ast as ast
|
||
|
import logging
|
||
|
import os
|
||
|
import re
|
||
|
|
||
|
|
||
|
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``. Pass a directory string as
|
||
|
``includeDir`` to explicitly declare a directory to search included feature files
|
||
|
in.
|
||
|
"""
|
||
|
|
||
|
extensions = {}
|
||
|
ast = ast
|
||
|
SS_FEATURE_TAGS = {"ss%02d" % i for i in range(1, 20 + 1)}
|
||
|
CV_FEATURE_TAGS = {"cv%02d" % i for i in range(1, 99 + 1)}
|
||
|
|
||
|
def __init__(
|
||
|
self, featurefile, glyphNames=(), followIncludes=True, includeDir=None, **kwargs
|
||
|
):
|
||
|
if "glyphMap" in kwargs:
|
||
|
from fontTools.misc.loggingTools import deprecateArgument
|
||
|
|
||
|
deprecateArgument("glyphMap", "use 'glyphNames' (iterable) instead")
|
||
|
if glyphNames:
|
||
|
raise TypeError(
|
||
|
"'glyphNames' and (deprecated) 'glyphMap' are " "mutually exclusive"
|
||
|
)
|
||
|
glyphNames = kwargs.pop("glyphMap")
|
||
|
if kwargs:
|
||
|
raise TypeError(
|
||
|
"unsupported keyword argument%s: %s"
|
||
|
% ("" if len(kwargs) == 1 else "s", ", ".join(repr(k) for k in kwargs))
|
||
|
)
|
||
|
|
||
|
self.glyphNames_ = set(glyphNames)
|
||
|
self.doc_ = self.ast.FeatureFile()
|
||
|
self.anchors_ = SymbolTable()
|
||
|
self.glyphclasses_ = SymbolTable()
|
||
|
self.lookups_ = SymbolTable()
|
||
|
self.valuerecords_ = SymbolTable()
|
||
|
self.symbol_tables_ = {self.anchors_, self.valuerecords_}
|
||
|
self.next_token_type_, self.next_token_ = (None, None)
|
||
|
self.cur_comments_ = []
|
||
|
self.next_token_location_ = None
|
||
|
lexerClass = IncludingLexer if followIncludes else NonIncludingLexer
|
||
|
self.lexer_ = lexerClass(featurefile, includeDir=includeDir)
|
||
|
self.missing = {}
|
||
|
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)
|
||
|
if self.cur_token_type_ is Lexer.COMMENT:
|
||
|
statements.append(
|
||
|
self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
|
||
|
)
|
||
|
elif self.is_cur_keyword_("include"):
|
||
|
statements.append(self.parse_include_())
|
||
|
elif self.cur_token_type_ is Lexer.GLYPHCLASS:
|
||
|
statements.append(self.parse_glyphclass_definition_())
|
||
|
elif self.is_cur_keyword_(("anon", "anonymous")):
|
||
|
statements.append(self.parse_anonymous_())
|
||
|
elif self.is_cur_keyword_("anchorDef"):
|
||
|
statements.append(self.parse_anchordef_())
|
||
|
elif self.is_cur_keyword_("languagesystem"):
|
||
|
statements.append(self.parse_languagesystem_())
|
||
|
elif self.is_cur_keyword_("lookup"):
|
||
|
statements.append(self.parse_lookup_(vertical=False))
|
||
|
elif self.is_cur_keyword_("markClass"):
|
||
|
statements.append(self.parse_markClass_())
|
||
|
elif self.is_cur_keyword_("feature"):
|
||
|
statements.append(self.parse_feature_block_())
|
||
|
elif self.is_cur_keyword_("conditionset"):
|
||
|
statements.append(self.parse_conditionset_())
|
||
|
elif self.is_cur_keyword_("variation"):
|
||
|
statements.append(self.parse_feature_block_(variation=True))
|
||
|
elif self.is_cur_keyword_("table"):
|
||
|
statements.append(self.parse_table_())
|
||
|
elif self.is_cur_keyword_("valueRecordDef"):
|
||
|
statements.append(self.parse_valuerecord_definition_(vertical=False))
|
||
|
elif (
|
||
|
self.cur_token_type_ is Lexer.NAME
|
||
|
and self.cur_token_ in self.extensions
|
||
|
):
|
||
|
statements.append(self.extensions[self.cur_token_](self))
|
||
|
elif self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == ";":
|
||
|
continue
|
||
|
else:
|
||
|
raise FeatureLibError(
|
||
|
"Expected feature, languagesystem, lookup, markClass, "
|
||
|
'table, or glyph class definition, got {} "{}"'.format(
|
||
|
self.cur_token_type_, self.cur_token_
|
||
|
),
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
# Report any missing glyphs at the end of parsing
|
||
|
if self.missing:
|
||
|
error = [
|
||
|
" %s (first found at %s)" % (name, loc)
|
||
|
for name, loc in self.missing.items()
|
||
|
]
|
||
|
raise FeatureLibError(
|
||
|
"The following glyph names are referenced but are missing from the "
|
||
|
"glyph set:\n" + ("\n".join(error)),
|
||
|
None,
|
||
|
)
|
||
|
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": # Format D
|
||
|
self.expect_keyword_("NULL")
|
||
|
self.expect_symbol_(">")
|
||
|
return None
|
||
|
|
||
|
if self.next_token_type_ == Lexer.NAME: # Format E
|
||
|
name = self.expect_name_()
|
||
|
anchordef = self.anchors_.resolve(name)
|
||
|
if anchordef is None:
|
||
|
raise FeatureLibError(
|
||
|
'Unknown anchor "%s"' % name, self.cur_token_location_
|
||
|
)
|
||
|
self.expect_symbol_(">")
|
||
|
return self.ast.Anchor(
|
||
|
anchordef.x,
|
||
|
anchordef.y,
|
||
|
name=name,
|
||
|
contourpoint=anchordef.contourpoint,
|
||
|
xDeviceTable=None,
|
||
|
yDeviceTable=None,
|
||
|
location=location,
|
||
|
)
|
||
|
|
||
|
x, y = self.expect_number_(variable=True), self.expect_number_(variable=True)
|
||
|
|
||
|
contourpoint = None
|
||
|
if self.next_token_ == "contourpoint": # Format B
|
||
|
self.expect_keyword_("contourpoint")
|
||
|
contourpoint = self.expect_number_()
|
||
|
|
||
|
if self.next_token_ == "<": # Format C
|
||
|
xDeviceTable = self.parse_device_()
|
||
|
yDeviceTable = self.parse_device_()
|
||
|
else:
|
||
|
xDeviceTable, yDeviceTable = None, None
|
||
|
|
||
|
self.expect_symbol_(">")
|
||
|
return self.ast.Anchor(
|
||
|
x,
|
||
|
y,
|
||
|
name=None,
|
||
|
contourpoint=contourpoint,
|
||
|
xDeviceTable=xDeviceTable,
|
||
|
yDeviceTable=yDeviceTable,
|
||
|
location=location,
|
||
|
)
|
||
|
|
||
|
def parse_anchor_marks_(self):
|
||
|
# Parses a sequence of ``[<anchor> mark @MARKCLASS]*.``
|
||
|
anchorMarks = [] # [(self.ast.Anchor, markClassName)*]
|
||
|
while self.next_token_ == "<":
|
||
|
anchor = self.parse_anchor_()
|
||
|
if anchor is None and self.next_token_ != "mark":
|
||
|
continue # <anchor NULL> without mark, eg. in GPOS type 5
|
||
|
self.expect_keyword_("mark")
|
||
|
markClass = self.expect_markClass_reference_()
|
||
|
anchorMarks.append((anchor, markClass))
|
||
|
return anchorMarks
|
||
|
|
||
|
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")
|
||
|
location = self.cur_token_location_
|
||
|
x, y = self.expect_number_(), self.expect_number_()
|
||
|
contourpoint = None
|
||
|
if self.next_token_ == "contourpoint":
|
||
|
self.expect_keyword_("contourpoint")
|
||
|
contourpoint = self.expect_number_()
|
||
|
name = self.expect_name_()
|
||
|
self.expect_symbol_(";")
|
||
|
anchordef = self.ast.AnchorDefinition(
|
||
|
name, x, y, contourpoint=contourpoint, location=location
|
||
|
)
|
||
|
self.anchors_.define(name, anchordef)
|
||
|
return anchordef
|
||
|
|
||
|
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"))
|
||
|
tag = self.expect_tag_()
|
||
|
_, content, location = self.lexer_.scan_anonymous_block(tag)
|
||
|
self.advance_lexer_()
|
||
|
self.expect_symbol_("}")
|
||
|
end_tag = self.expect_tag_()
|
||
|
assert tag == end_tag, "bad splitting in Lexer.scan_anonymous_block()"
|
||
|
self.expect_symbol_(";")
|
||
|
return self.ast.AnonymousBlock(tag, content, location=location)
|
||
|
|
||
|
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")
|
||
|
location = self.cur_token_location_
|
||
|
glyphs = self.parse_glyphclass_(accept_glyphname=True)
|
||
|
contourPoints = {self.expect_number_()}
|
||
|
while self.next_token_ != ";":
|
||
|
contourPoints.add(self.expect_number_())
|
||
|
self.expect_symbol_(";")
|
||
|
return self.ast.AttachStatement(glyphs, contourPoints, location=location)
|
||
|
|
||
|
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"}
|
||
|
self.advance_lexer_()
|
||
|
return self.parse_position_(enumerated=True, vertical=vertical)
|
||
|
|
||
|
def parse_GlyphClassDef_(self):
|
||
|
# Parses 'GlyphClassDef @BASE, @LIGATURES, @MARKS, @COMPONENTS;'
|
||
|
assert self.is_cur_keyword_("GlyphClassDef")
|
||
|
location = self.cur_token_location_
|
||
|
if self.next_token_ != ",":
|
||
|
baseGlyphs = self.parse_glyphclass_(accept_glyphname=False)
|
||
|
else:
|
||
|
baseGlyphs = None
|
||
|
self.expect_symbol_(",")
|
||
|
if self.next_token_ != ",":
|
||
|
ligatureGlyphs = self.parse_glyphclass_(accept_glyphname=False)
|
||
|
else:
|
||
|
ligatureGlyphs = None
|
||
|
self.expect_symbol_(",")
|
||
|
if self.next_token_ != ",":
|
||
|
markGlyphs = self.parse_glyphclass_(accept_glyphname=False)
|
||
|
else:
|
||
|
markGlyphs = None
|
||
|
self.expect_symbol_(",")
|
||
|
if self.next_token_ != ";":
|
||
|
componentGlyphs = self.parse_glyphclass_(accept_glyphname=False)
|
||
|
else:
|
||
|
componentGlyphs = None
|
||
|
self.expect_symbol_(";")
|
||
|
return self.ast.GlyphClassDefStatement(
|
||
|
baseGlyphs, markGlyphs, ligatureGlyphs, componentGlyphs, location=location
|
||
|
)
|
||
|
|
||
|
def parse_glyphclass_definition_(self):
|
||
|
# 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)
|
||
|
self.expect_symbol_(";")
|
||
|
glyphclass = self.ast.GlyphClassDefinition(name, glyphs, location=location)
|
||
|
self.glyphclasses_.define(name, glyphclass)
|
||
|
return glyphclass
|
||
|
|
||
|
def split_glyph_range_(self, name, location):
|
||
|
# Since v1.20, the OpenType Feature File specification allows
|
||
|
# for dashes in glyph names. A sequence like "a-b-c-d" could
|
||
|
# therefore mean a single glyph whose name happens to be
|
||
|
# "a-b-c-d", or it could mean a range from glyph "a" to glyph
|
||
|
# "b-c-d", or a range from glyph "a-b" to glyph "c-d", or a
|
||
|
# range from glyph "a-b-c" to glyph "d".Technically, this
|
||
|
# example could be resolved because the (pretty complex)
|
||
|
# definition of glyph ranges renders most of these splits
|
||
|
# invalid. But the specification does not say that a compiler
|
||
|
# should try to apply such fancy heuristics. To encourage
|
||
|
# unambiguous feature files, we therefore try all possible
|
||
|
# splits and reject the feature file if there are multiple
|
||
|
# splits possible. It is intentional that we don't just emit a
|
||
|
# warning; warnings tend to get ignored. To fix the problem,
|
||
|
# font designers can trivially add spaces around the intended
|
||
|
# split point, and we emit a compiler error that suggests
|
||
|
# how exactly the source should be rewritten to make things
|
||
|
# unambiguous.
|
||
|
parts = name.split("-")
|
||
|
solutions = []
|
||
|
for i in range(len(parts)):
|
||
|
start, limit = "-".join(parts[0:i]), "-".join(parts[i:])
|
||
|
if start in self.glyphNames_ and limit in self.glyphNames_:
|
||
|
solutions.append((start, limit))
|
||
|
if len(solutions) == 1:
|
||
|
start, limit = solutions[0]
|
||
|
return start, limit
|
||
|
elif len(solutions) == 0:
|
||
|
raise FeatureLibError(
|
||
|
'"%s" is not a glyph in the font, and it can not be split '
|
||
|
"into a range of known glyphs" % name,
|
||
|
location,
|
||
|
)
|
||
|
else:
|
||
|
ranges = " or ".join(['"%s - %s"' % (s, l) for s, l in solutions])
|
||
|
raise FeatureLibError(
|
||
|
'Ambiguous glyph range "%s"; '
|
||
|
"please use %s to clarify what you mean" % (name, ranges),
|
||
|
location,
|
||
|
)
|
||
|
|
||
|
def parse_glyphclass_(self, accept_glyphname, accept_null=False):
|
||
|
# Parses a glyph class, either named or anonymous, or (if
|
||
|
# ``bool(accept_glyphname)``) a glyph name. If ``bool(accept_null)`` then
|
||
|
# also accept the special NULL glyph.
|
||
|
if accept_glyphname and self.next_token_type_ in (Lexer.NAME, Lexer.CID):
|
||
|
if accept_null and self.next_token_ == "NULL":
|
||
|
# If you want a glyph called NULL, you should escape it.
|
||
|
self.advance_lexer_()
|
||
|
return self.ast.NullGlyph(location=self.cur_token_location_)
|
||
|
glyph = self.expect_glyph_()
|
||
|
self.check_glyph_name_in_glyph_set(glyph)
|
||
|
return self.ast.GlyphName(glyph, location=self.cur_token_location_)
|
||
|
if self.next_token_type_ is Lexer.GLYPHCLASS:
|
||
|
self.advance_lexer_()
|
||
|
gc = self.glyphclasses_.resolve(self.cur_token_)
|
||
|
if gc is None:
|
||
|
raise FeatureLibError(
|
||
|
"Unknown glyph class @%s" % self.cur_token_,
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
if isinstance(gc, self.ast.MarkClass):
|
||
|
return self.ast.MarkClassName(gc, location=self.cur_token_location_)
|
||
|
else:
|
||
|
return self.ast.GlyphClassName(gc, location=self.cur_token_location_)
|
||
|
|
||
|
self.expect_symbol_("[")
|
||
|
location = self.cur_token_location_
|
||
|
glyphs = self.ast.GlyphClass(location=location)
|
||
|
while self.next_token_ != "]":
|
||
|
if self.next_token_type_ is Lexer.NAME:
|
||
|
glyph = self.expect_glyph_()
|
||
|
location = self.cur_token_location_
|
||
|
if "-" in glyph and self.glyphNames_ and glyph not in self.glyphNames_:
|
||
|
start, limit = self.split_glyph_range_(glyph, location)
|
||
|
self.check_glyph_name_in_glyph_set(start, limit)
|
||
|
glyphs.add_range(
|
||
|
start, limit, self.make_glyph_range_(location, start, limit)
|
||
|
)
|
||
|
elif self.next_token_ == "-":
|
||
|
start = glyph
|
||
|
self.expect_symbol_("-")
|
||
|
limit = self.expect_glyph_()
|
||
|
self.check_glyph_name_in_glyph_set(start, limit)
|
||
|
glyphs.add_range(
|
||
|
start, limit, self.make_glyph_range_(location, start, limit)
|
||
|
)
|
||
|
else:
|
||
|
if "-" in glyph and not self.glyphNames_:
|
||
|
log.warning(
|
||
|
str(
|
||
|
FeatureLibError(
|
||
|
f"Ambiguous glyph name that looks like a range: {glyph!r}",
|
||
|
location,
|
||
|
)
|
||
|
)
|
||
|
)
|
||
|
self.check_glyph_name_in_glyph_set(glyph)
|
||
|
glyphs.append(glyph)
|
||
|
elif self.next_token_type_ is Lexer.CID:
|
||
|
glyph = self.expect_glyph_()
|
||
|
if self.next_token_ == "-":
|
||
|
range_location = self.cur_token_location_
|
||
|
range_start = self.cur_token_
|
||
|
self.expect_symbol_("-")
|
||
|
range_end = self.expect_cid_()
|
||
|
self.check_glyph_name_in_glyph_set(
|
||
|
f"cid{range_start:05d}",
|
||
|
f"cid{range_end:05d}",
|
||
|
)
|
||
|
glyphs.add_cid_range(
|
||
|
range_start,
|
||
|
range_end,
|
||
|
self.make_cid_range_(range_location, range_start, range_end),
|
||
|
)
|
||
|
else:
|
||
|
glyph_name = f"cid{self.cur_token_:05d}"
|
||
|
self.check_glyph_name_in_glyph_set(glyph_name)
|
||
|
glyphs.append(glyph_name)
|
||
|
elif self.next_token_type_ is Lexer.GLYPHCLASS:
|
||
|
self.advance_lexer_()
|
||
|
gc = self.glyphclasses_.resolve(self.cur_token_)
|
||
|
if gc is None:
|
||
|
raise FeatureLibError(
|
||
|
"Unknown glyph class @%s" % self.cur_token_,
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
if isinstance(gc, self.ast.MarkClass):
|
||
|
gc = self.ast.MarkClassName(gc, location=self.cur_token_location_)
|
||
|
else:
|
||
|
gc = self.ast.GlyphClassName(gc, location=self.cur_token_location_)
|
||
|
glyphs.add_class(gc)
|
||
|
else:
|
||
|
raise FeatureLibError(
|
||
|
"Expected glyph name, glyph range, "
|
||
|
f"or glyph class reference, found {self.next_token_!r}",
|
||
|
self.next_token_location_,
|
||
|
)
|
||
|
self.expect_symbol_("]")
|
||
|
return glyphs
|
||
|
|
||
|
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", ";", ","}:
|
||
|
gc = self.parse_glyphclass_(accept_glyphname=True)
|
||
|
marked = False
|
||
|
if self.next_token_ == "'":
|
||
|
self.expect_symbol_("'")
|
||
|
hasMarks = marked = True
|
||
|
if marked:
|
||
|
if suffix:
|
||
|
# makeotf also reports this as an error, while FontForge
|
||
|
# silently inserts ' in all the intervening glyphs.
|
||
|
# https://github.com/fonttools/fonttools/pull/1096
|
||
|
raise FeatureLibError(
|
||
|
"Unsupported contextual target sequence: at most "
|
||
|
"one run of marked (') glyph/class names allowed",
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
glyphs.append(gc)
|
||
|
elif glyphs:
|
||
|
suffix.append(gc)
|
||
|
else:
|
||
|
prefix.append(gc)
|
||
|
|
||
|
if self.is_next_value_():
|
||
|
values.append(self.parse_valuerecord_(vertical))
|
||
|
else:
|
||
|
values.append(None)
|
||
|
|
||
|
lookuplist = None
|
||
|
while self.next_token_ == "lookup":
|
||
|
if lookuplist is None:
|
||
|
lookuplist = []
|
||
|
self.expect_keyword_("lookup")
|
||
|
if not marked:
|
||
|
raise FeatureLibError(
|
||
|
"Lookups can only follow marked glyphs",
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
lookup_name = self.expect_name_()
|
||
|
lookup = self.lookups_.resolve(lookup_name)
|
||
|
if lookup is None:
|
||
|
raise FeatureLibError(
|
||
|
'Unknown lookup "%s"' % lookup_name, self.cur_token_location_
|
||
|
)
|
||
|
lookuplist.append(lookup)
|
||
|
if marked:
|
||
|
lookups.append(lookuplist)
|
||
|
|
||
|
if not glyphs and not suffix: # eg., "sub f f i by"
|
||
|
assert lookups == []
|
||
|
return ([], prefix, [None] * len(prefix), values, [], hasMarks)
|
||
|
else:
|
||
|
if any(values[: len(prefix)]):
|
||
|
raise FeatureLibError(
|
||
|
"Positioning cannot be applied in the bactrack glyph sequence, "
|
||
|
"before the marked glyph sequence.",
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
marked_values = values[len(prefix) : len(prefix) + len(glyphs)]
|
||
|
if any(marked_values):
|
||
|
if any(values[len(prefix) + len(glyphs) :]):
|
||
|
raise FeatureLibError(
|
||
|
"Positioning values are allowed only in the marked glyph "
|
||
|
"sequence, or after the final glyph node when only one glyph "
|
||
|
"node is marked.",
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
values = marked_values
|
||
|
elif values and values[-1]:
|
||
|
if len(glyphs) > 1 or any(values[:-1]):
|
||
|
raise FeatureLibError(
|
||
|
"Positioning values are allowed only in the marked glyph "
|
||
|
"sequence, or after the final glyph node when only one glyph "
|
||
|
"node is marked.",
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
values = values[-1:]
|
||
|
elif any(values):
|
||
|
raise FeatureLibError(
|
||
|
"Positioning values are allowed only in the marked glyph "
|
||
|
"sequence, or after the final glyph node when only one glyph "
|
||
|
"node is marked.",
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
return (prefix, glyphs, lookups, values, suffix, hasMarks)
|
||
|
|
||
|
def parse_ignore_glyph_pattern_(self, sub):
|
||
|
location = self.cur_token_location_
|
||
|
prefix, glyphs, lookups, values, suffix, hasMarks = self.parse_glyph_pattern_(
|
||
|
vertical=False
|
||
|
)
|
||
|
if any(lookups):
|
||
|
raise FeatureLibError(
|
||
|
f'No lookups can be specified for "ignore {sub}"', location
|
||
|
)
|
||
|
if not hasMarks:
|
||
|
error = FeatureLibError(
|
||
|
f'Ambiguous "ignore {sub}", there should be least one marked glyph',
|
||
|
location,
|
||
|
)
|
||
|
log.warning(str(error))
|
||
|
suffix, glyphs = glyphs[1:], glyphs[0:1]
|
||
|
chainContext = (prefix, glyphs, suffix)
|
||
|
return chainContext
|
||
|
|
||
|
def parse_ignore_context_(self, sub):
|
||
|
location = self.cur_token_location_
|
||
|
chainContext = [self.parse_ignore_glyph_pattern_(sub)]
|
||
|
while self.next_token_ == ",":
|
||
|
self.expect_symbol_(",")
|
||
|
chainContext.append(self.parse_ignore_glyph_pattern_(sub))
|
||
|
self.expect_symbol_(";")
|
||
|
return chainContext
|
||
|
|
||
|
def parse_ignore_(self):
|
||
|
# Parses an ignore sub/pos rule.
|
||
|
assert self.is_cur_keyword_("ignore")
|
||
|
location = self.cur_token_location_
|
||
|
self.advance_lexer_()
|
||
|
if self.cur_token_ in ["substitute", "sub"]:
|
||
|
chainContext = self.parse_ignore_context_("sub")
|
||
|
return self.ast.IgnoreSubstStatement(chainContext, location=location)
|
||
|
if self.cur_token_ in ["position", "pos"]:
|
||
|
chainContext = self.parse_ignore_context_("pos")
|
||
|
return self.ast.IgnorePosStatement(chainContext, location=location)
|
||
|
raise FeatureLibError(
|
||
|
'Expected "substitute" or "position"', self.cur_token_location_
|
||
|
)
|
||
|
|
||
|
def parse_include_(self):
|
||
|
assert self.cur_token_ == "include"
|
||
|
location = self.cur_token_location_
|
||
|
filename = self.expect_filename_()
|
||
|
# self.expect_symbol_(";")
|
||
|
return ast.IncludeStatement(filename, location=location)
|
||
|
|
||
|
def parse_language_(self):
|
||
|
assert self.is_cur_keyword_("language")
|
||
|
location = self.cur_token_location_
|
||
|
language = self.expect_language_tag_()
|
||
|
include_default, required = (True, False)
|
||
|
if self.next_token_ in {"exclude_dflt", "include_dflt"}:
|
||
|
include_default = self.expect_name_() == "include_dflt"
|
||
|
if self.next_token_ == "required":
|
||
|
self.expect_keyword_("required")
|
||
|
required = True
|
||
|
self.expect_symbol_(";")
|
||
|
return self.ast.LanguageStatement(
|
||
|
language, include_default, required, location=location
|
||
|
)
|
||
|
|
||
|
def parse_ligatureCaretByIndex_(self):
|
||
|
assert self.is_cur_keyword_("LigatureCaretByIndex")
|
||
|
location = self.cur_token_location_
|
||
|
glyphs = self.parse_glyphclass_(accept_glyphname=True)
|
||
|
carets = [self.expect_number_()]
|
||
|
while self.next_token_ != ";":
|
||
|
carets.append(self.expect_number_())
|
||
|
self.expect_symbol_(";")
|
||
|
return self.ast.LigatureCaretByIndexStatement(glyphs, carets, location=location)
|
||
|
|
||
|
def parse_ligatureCaretByPos_(self):
|
||
|
assert self.is_cur_keyword_("LigatureCaretByPos")
|
||
|
location = self.cur_token_location_
|
||
|
glyphs = self.parse_glyphclass_(accept_glyphname=True)
|
||
|
carets = [self.expect_number_(variable=True)]
|
||
|
while self.next_token_ != ";":
|
||
|
carets.append(self.expect_number_(variable=True))
|
||
|
self.expect_symbol_(";")
|
||
|
return self.ast.LigatureCaretByPosStatement(glyphs, carets, 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_()
|
||
|
|
||
|
if self.next_token_ == ";":
|
||
|
lookup = self.lookups_.resolve(name)
|
||
|
if lookup is None:
|
||
|
raise FeatureLibError(
|
||
|
'Unknown lookup "%s"' % name, self.cur_token_location_
|
||
|
)
|
||
|
self.expect_symbol_(";")
|
||
|
return self.ast.LookupReferenceStatement(lookup, location=location)
|
||
|
|
||
|
use_extension = False
|
||
|
if self.next_token_ == "useExtension":
|
||
|
self.expect_keyword_("useExtension")
|
||
|
use_extension = True
|
||
|
|
||
|
block = self.ast.LookupBlock(name, use_extension, location=location)
|
||
|
self.parse_block_(block, vertical)
|
||
|
self.lookups_.define(name, block)
|
||
|
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_
|
||
|
|
||
|
# format B: "lookupflag 6;"
|
||
|
if self.next_token_type_ == Lexer.NUMBER:
|
||
|
value = self.expect_number_()
|
||
|
self.expect_symbol_(";")
|
||
|
return self.ast.LookupFlagStatement(value, location=location)
|
||
|
|
||
|
# format A: "lookupflag RightToLeft MarkAttachmentType @M;"
|
||
|
value_seen = False
|
||
|
value, markAttachment, markFilteringSet = 0, None, None
|
||
|
flags = {
|
||
|
"RightToLeft": 1,
|
||
|
"IgnoreBaseGlyphs": 2,
|
||
|
"IgnoreLigatures": 4,
|
||
|
"IgnoreMarks": 8,
|
||
|
}
|
||
|
seen = set()
|
||
|
while self.next_token_ != ";":
|
||
|
if self.next_token_ in seen:
|
||
|
raise FeatureLibError(
|
||
|
"%s can be specified only once" % self.next_token_,
|
||
|
self.next_token_location_,
|
||
|
)
|
||
|
seen.add(self.next_token_)
|
||
|
if self.next_token_ == "MarkAttachmentType":
|
||
|
self.expect_keyword_("MarkAttachmentType")
|
||
|
markAttachment = self.parse_glyphclass_(accept_glyphname=False)
|
||
|
elif self.next_token_ == "UseMarkFilteringSet":
|
||
|
self.expect_keyword_("UseMarkFilteringSet")
|
||
|
markFilteringSet = self.parse_glyphclass_(accept_glyphname=False)
|
||
|
elif self.next_token_ in flags:
|
||
|
value_seen = True
|
||
|
value = value | flags[self.expect_name_()]
|
||
|
else:
|
||
|
raise FeatureLibError(
|
||
|
'"%s" is not a recognized lookupflag' % self.next_token_,
|
||
|
self.next_token_location_,
|
||
|
)
|
||
|
self.expect_symbol_(";")
|
||
|
|
||
|
if not any([value_seen, markAttachment, markFilteringSet]):
|
||
|
raise FeatureLibError(
|
||
|
"lookupflag must have a value", self.next_token_location_
|
||
|
)
|
||
|
|
||
|
return self.ast.LookupFlagStatement(
|
||
|
value,
|
||
|
markAttachment=markAttachment,
|
||
|
markFilteringSet=markFilteringSet,
|
||
|
location=location,
|
||
|
)
|
||
|
|
||
|
def parse_markClass_(self):
|
||
|
assert self.is_cur_keyword_("markClass")
|
||
|
location = self.cur_token_location_
|
||
|
glyphs = self.parse_glyphclass_(accept_glyphname=True)
|
||
|
if not glyphs.glyphSet():
|
||
|
raise FeatureLibError(
|
||
|
"Empty glyph class in mark class definition", location
|
||
|
)
|
||
|
anchor = self.parse_anchor_()
|
||
|
name = self.expect_class_name_()
|
||
|
self.expect_symbol_(";")
|
||
|
markClass = self.doc_.markClasses.get(name)
|
||
|
if markClass is None:
|
||
|
markClass = self.ast.MarkClass(name)
|
||
|
self.doc_.markClasses[name] = markClass
|
||
|
self.glyphclasses_.define(name, markClass)
|
||
|
mcdef = self.ast.MarkClassDefinition(
|
||
|
markClass, anchor, glyphs, location=location
|
||
|
)
|
||
|
markClass.addDefinition(mcdef)
|
||
|
return mcdef
|
||
|
|
||
|
def parse_position_(self, enumerated, vertical):
|
||
|
assert self.cur_token_ in {"position", "pos"}
|
||
|
if self.next_token_ == "cursive": # GPOS type 3
|
||
|
return self.parse_position_cursive_(enumerated, vertical)
|
||
|
elif self.next_token_ == "base": # GPOS type 4
|
||
|
return self.parse_position_base_(enumerated, vertical)
|
||
|
elif self.next_token_ == "ligature": # GPOS type 5
|
||
|
return self.parse_position_ligature_(enumerated, vertical)
|
||
|
elif self.next_token_ == "mark": # GPOS type 6
|
||
|
return self.parse_position_mark_(enumerated, vertical)
|
||
|
|
||
|
location = self.cur_token_location_
|
||
|
prefix, glyphs, lookups, values, suffix, hasMarks = self.parse_glyph_pattern_(
|
||
|
vertical
|
||
|
)
|
||
|
self.expect_symbol_(";")
|
||
|
|
||
|
if any(lookups):
|
||
|
# GPOS type 8: Chaining contextual positioning; explicit lookups
|
||
|
if any(values):
|
||
|
raise FeatureLibError(
|
||
|
'If "lookup" is present, no values must be specified', location
|
||
|
)
|
||
|
return self.ast.ChainContextPosStatement(
|
||
|
prefix, glyphs, suffix, lookups, location=location
|
||
|
)
|
||
|
|
||
|
# Pair positioning, format A: "pos V 10 A -10;"
|
||
|
# Pair positioning, format B: "pos V A -20;"
|
||
|
if not prefix and not suffix and len(glyphs) == 2 and not hasMarks:
|
||
|
if values[0] is None: # Format B: "pos V A -20;"
|
||
|
values.reverse()
|
||
|
return self.ast.PairPosStatement(
|
||
|
glyphs[0],
|
||
|
values[0],
|
||
|
glyphs[1],
|
||
|
values[1],
|
||
|
enumerated=enumerated,
|
||
|
location=location,
|
||
|
)
|
||
|
|
||
|
if enumerated:
|
||
|
raise FeatureLibError(
|
||
|
'"enumerate" is only allowed with pair positionings', location
|
||
|
)
|
||
|
return self.ast.SinglePosStatement(
|
||
|
list(zip(glyphs, values)),
|
||
|
prefix,
|
||
|
suffix,
|
||
|
forceChain=hasMarks,
|
||
|
location=location,
|
||
|
)
|
||
|
|
||
|
def parse_position_cursive_(self, enumerated, vertical):
|
||
|
location = self.cur_token_location_
|
||
|
self.expect_keyword_("cursive")
|
||
|
if enumerated:
|
||
|
raise FeatureLibError(
|
||
|
'"enumerate" is not allowed with ' "cursive attachment positioning",
|
||
|
location,
|
||
|
)
|
||
|
glyphclass = self.parse_glyphclass_(accept_glyphname=True)
|
||
|
entryAnchor = self.parse_anchor_()
|
||
|
exitAnchor = self.parse_anchor_()
|
||
|
self.expect_symbol_(";")
|
||
|
return self.ast.CursivePosStatement(
|
||
|
glyphclass, entryAnchor, exitAnchor, location=location
|
||
|
)
|
||
|
|
||
|
def parse_position_base_(self, enumerated, vertical):
|
||
|
location = self.cur_token_location_
|
||
|
self.expect_keyword_("base")
|
||
|
if enumerated:
|
||
|
raise FeatureLibError(
|
||
|
'"enumerate" is not allowed with '
|
||
|
"mark-to-base attachment positioning",
|
||
|
location,
|
||
|
)
|
||
|
base = self.parse_glyphclass_(accept_glyphname=True)
|
||
|
marks = self.parse_anchor_marks_()
|
||
|
self.expect_symbol_(";")
|
||
|
return self.ast.MarkBasePosStatement(base, marks, location=location)
|
||
|
|
||
|
def parse_position_ligature_(self, enumerated, vertical):
|
||
|
location = self.cur_token_location_
|
||
|
self.expect_keyword_("ligature")
|
||
|
if enumerated:
|
||
|
raise FeatureLibError(
|
||
|
'"enumerate" is not allowed with '
|
||
|
"mark-to-ligature attachment positioning",
|
||
|
location,
|
||
|
)
|
||
|
ligatures = self.parse_glyphclass_(accept_glyphname=True)
|
||
|
marks = [self.parse_anchor_marks_()]
|
||
|
while self.next_token_ == "ligComponent":
|
||
|
self.expect_keyword_("ligComponent")
|
||
|
marks.append(self.parse_anchor_marks_())
|
||
|
self.expect_symbol_(";")
|
||
|
return self.ast.MarkLigPosStatement(ligatures, marks, location=location)
|
||
|
|
||
|
def parse_position_mark_(self, enumerated, vertical):
|
||
|
location = self.cur_token_location_
|
||
|
self.expect_keyword_("mark")
|
||
|
if enumerated:
|
||
|
raise FeatureLibError(
|
||
|
'"enumerate" is not allowed with '
|
||
|
"mark-to-mark attachment positioning",
|
||
|
location,
|
||
|
)
|
||
|
baseMarks = self.parse_glyphclass_(accept_glyphname=True)
|
||
|
marks = self.parse_anchor_marks_()
|
||
|
self.expect_symbol_(";")
|
||
|
return self.ast.MarkMarkPosStatement(baseMarks, marks, location=location)
|
||
|
|
||
|
def parse_script_(self):
|
||
|
assert self.is_cur_keyword_("script")
|
||
|
location, script = self.cur_token_location_, self.expect_script_tag_()
|
||
|
self.expect_symbol_(";")
|
||
|
return self.ast.ScriptStatement(script, location=location)
|
||
|
|
||
|
def parse_substitute_(self):
|
||
|
assert self.cur_token_ in {"substitute", "sub", "reversesub", "rsub"}
|
||
|
location = self.cur_token_location_
|
||
|
reverse = self.cur_token_ in {"reversesub", "rsub"}
|
||
|
(
|
||
|
old_prefix,
|
||
|
old,
|
||
|
lookups,
|
||
|
values,
|
||
|
old_suffix,
|
||
|
hasMarks,
|
||
|
) = self.parse_glyph_pattern_(vertical=False)
|
||
|
if any(values):
|
||
|
raise FeatureLibError(
|
||
|
"Substitution statements cannot contain values", location
|
||
|
)
|
||
|
new = []
|
||
|
if self.next_token_ == "by":
|
||
|
keyword = self.expect_keyword_("by")
|
||
|
while self.next_token_ != ";":
|
||
|
gc = self.parse_glyphclass_(accept_glyphname=True, accept_null=True)
|
||
|
new.append(gc)
|
||
|
elif self.next_token_ == "from":
|
||
|
keyword = self.expect_keyword_("from")
|
||
|
new = [self.parse_glyphclass_(accept_glyphname=False)]
|
||
|
else:
|
||
|
keyword = None
|
||
|
self.expect_symbol_(";")
|
||
|
if len(new) == 0 and not any(lookups):
|
||
|
raise FeatureLibError(
|
||
|
'Expected "by", "from" or explicit lookup references',
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
|
||
|
# GSUB lookup type 3: Alternate substitution.
|
||
|
# Format: "substitute a from [a.1 a.2 a.3];"
|
||
|
if keyword == "from":
|
||
|
if reverse:
|
||
|
raise FeatureLibError(
|
||
|
'Reverse chaining substitutions do not support "from"', location
|
||
|
)
|
||
|
if len(old) != 1 or len(old[0].glyphSet()) != 1:
|
||
|
raise FeatureLibError('Expected a single glyph before "from"', location)
|
||
|
if len(new) != 1:
|
||
|
raise FeatureLibError(
|
||
|
'Expected a single glyphclass after "from"', location
|
||
|
)
|
||
|
return self.ast.AlternateSubstStatement(
|
||
|
old_prefix, old[0], old_suffix, new[0], location=location
|
||
|
)
|
||
|
|
||
|
num_lookups = len([l for l in lookups if l is not None])
|
||
|
|
||
|
is_deletion = False
|
||
|
if len(new) == 1 and isinstance(new[0], ast.NullGlyph):
|
||
|
new = [] # Deletion
|
||
|
is_deletion = True
|
||
|
|
||
|
# GSUB lookup type 1: Single substitution.
|
||
|
# Format A: "substitute a by a.sc;"
|
||
|
# Format B: "substitute [one.fitted one.oldstyle] by one;"
|
||
|
# Format C: "substitute [a-d] by [A.sc-D.sc];"
|
||
|
if not reverse and len(old) == 1 and len(new) == 1 and num_lookups == 0:
|
||
|
glyphs = list(old[0].glyphSet())
|
||
|
replacements = list(new[0].glyphSet())
|
||
|
if len(replacements) == 1:
|
||
|
replacements = replacements * len(glyphs)
|
||
|
if len(glyphs) != len(replacements):
|
||
|
raise FeatureLibError(
|
||
|
'Expected a glyph class with %d elements after "by", '
|
||
|
"but found a glyph class with %d elements"
|
||
|
% (len(glyphs), len(replacements)),
|
||
|
location,
|
||
|
)
|
||
|
return self.ast.SingleSubstStatement(
|
||
|
old, new, old_prefix, old_suffix, forceChain=hasMarks, location=location
|
||
|
)
|
||
|
|
||
|
# Glyph deletion, built as GSUB lookup type 2: Multiple substitution
|
||
|
# with empty replacement.
|
||
|
if is_deletion and len(old) == 1 and num_lookups == 0:
|
||
|
return self.ast.MultipleSubstStatement(
|
||
|
old_prefix,
|
||
|
old[0],
|
||
|
old_suffix,
|
||
|
(),
|
||
|
forceChain=hasMarks,
|
||
|
location=location,
|
||
|
)
|
||
|
|
||
|
# GSUB lookup type 2: Multiple substitution.
|
||
|
# Format: "substitute f_f_i by f f i;"
|
||
|
#
|
||
|
# GlyphsApp introduces two additional formats:
|
||
|
# Format 1: "substitute [f_i f_l] by [f f] [i l];"
|
||
|
# Format 2: "substitute [f_i f_l] by f [i l];"
|
||
|
# http://handbook.glyphsapp.com/en/layout/multiple-substitution-with-classes/
|
||
|
if not reverse and len(old) == 1 and len(new) > 1 and num_lookups == 0:
|
||
|
count = len(old[0].glyphSet())
|
||
|
for n in new:
|
||
|
if not list(n.glyphSet()):
|
||
|
raise FeatureLibError("Empty class in replacement", location)
|
||
|
if len(n.glyphSet()) != 1 and len(n.glyphSet()) != count:
|
||
|
raise FeatureLibError(
|
||
|
f'Expected a glyph class with 1 or {count} elements after "by", '
|
||
|
f"but found a glyph class with {len(n.glyphSet())} elements",
|
||
|
location,
|
||
|
)
|
||
|
return self.ast.MultipleSubstStatement(
|
||
|
old_prefix,
|
||
|
old[0],
|
||
|
old_suffix,
|
||
|
new,
|
||
|
forceChain=hasMarks,
|
||
|
location=location,
|
||
|
)
|
||
|
|
||
|
# GSUB lookup type 4: Ligature substitution.
|
||
|
# Format: "substitute f f i by f_f_i;"
|
||
|
if (
|
||
|
not reverse
|
||
|
and len(old) > 1
|
||
|
and len(new) == 1
|
||
|
and len(new[0].glyphSet()) == 1
|
||
|
and num_lookups == 0
|
||
|
):
|
||
|
return self.ast.LigatureSubstStatement(
|
||
|
old_prefix,
|
||
|
old,
|
||
|
old_suffix,
|
||
|
list(new[0].glyphSet())[0],
|
||
|
forceChain=hasMarks,
|
||
|
location=location,
|
||
|
)
|
||
|
|
||
|
# GSUB lookup type 8: Reverse chaining substitution.
|
||
|
if reverse:
|
||
|
if len(old) != 1:
|
||
|
raise FeatureLibError(
|
||
|
"In reverse chaining single substitutions, "
|
||
|
"only a single glyph or glyph class can be replaced",
|
||
|
location,
|
||
|
)
|
||
|
if len(new) != 1:
|
||
|
raise FeatureLibError(
|
||
|
"In reverse chaining single substitutions, "
|
||
|
'the replacement (after "by") must be a single glyph '
|
||
|
"or glyph class",
|
||
|
location,
|
||
|
)
|
||
|
if num_lookups != 0:
|
||
|
raise FeatureLibError(
|
||
|
"Reverse chaining substitutions cannot call named lookups", location
|
||
|
)
|
||
|
glyphs = sorted(list(old[0].glyphSet()))
|
||
|
replacements = sorted(list(new[0].glyphSet()))
|
||
|
if len(replacements) == 1:
|
||
|
replacements = replacements * len(glyphs)
|
||
|
if len(glyphs) != len(replacements):
|
||
|
raise FeatureLibError(
|
||
|
'Expected a glyph class with %d elements after "by", '
|
||
|
"but found a glyph class with %d elements"
|
||
|
% (len(glyphs), len(replacements)),
|
||
|
location,
|
||
|
)
|
||
|
return self.ast.ReverseChainSingleSubstStatement(
|
||
|
old_prefix, old_suffix, old, new, location=location
|
||
|
)
|
||
|
|
||
|
if len(old) > 1 and len(new) > 1:
|
||
|
raise FeatureLibError(
|
||
|
"Direct substitution of multiple glyphs by multiple glyphs "
|
||
|
"is not supported",
|
||
|
location,
|
||
|
)
|
||
|
|
||
|
# If there are remaining glyphs to parse, this is an invalid GSUB statement
|
||
|
if len(new) != 0 or is_deletion:
|
||
|
raise FeatureLibError("Invalid substitution statement", location)
|
||
|
|
||
|
# GSUB lookup type 6: Chaining contextual substitution.
|
||
|
rule = self.ast.ChainContextSubstStatement(
|
||
|
old_prefix, old, old_suffix, lookups, location=location
|
||
|
)
|
||
|
return rule
|
||
|
|
||
|
def parse_subtable_(self):
|
||
|
assert self.is_cur_keyword_("subtable")
|
||
|
location = self.cur_token_location_
|
||
|
self.expect_symbol_(";")
|
||
|
return self.ast.SubtableStatement(location=location)
|
||
|
|
||
|
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")
|
||
|
location = self.cur_token_location_
|
||
|
DesignSize = self.expect_decipoint_()
|
||
|
SubfamilyID = self.expect_number_()
|
||
|
RangeStart = 0.0
|
||
|
RangeEnd = 0.0
|
||
|
if self.next_token_type_ in (Lexer.NUMBER, Lexer.FLOAT) or SubfamilyID != 0:
|
||
|
RangeStart = self.expect_decipoint_()
|
||
|
RangeEnd = self.expect_decipoint_()
|
||
|
|
||
|
self.expect_symbol_(";")
|
||
|
return self.ast.SizeParameters(
|
||
|
DesignSize, SubfamilyID, RangeStart, RangeEnd, location=location
|
||
|
)
|
||
|
|
||
|
def parse_size_menuname_(self):
|
||
|
assert self.is_cur_keyword_("sizemenuname")
|
||
|
location = self.cur_token_location_
|
||
|
platformID, platEncID, langID, string = self.parse_name_()
|
||
|
return self.ast.FeatureNameStatement(
|
||
|
"size", platformID, platEncID, langID, string, location=location
|
||
|
)
|
||
|
|
||
|
def parse_table_(self):
|
||
|
assert self.is_cur_keyword_("table")
|
||
|
location, name = self.cur_token_location_, self.expect_tag_()
|
||
|
table = self.ast.TableBlock(name, location=location)
|
||
|
self.expect_symbol_("{")
|
||
|
handler = {
|
||
|
"GDEF": self.parse_table_GDEF_,
|
||
|
"head": self.parse_table_head_,
|
||
|
"hhea": self.parse_table_hhea_,
|
||
|
"vhea": self.parse_table_vhea_,
|
||
|
"name": self.parse_table_name_,
|
||
|
"BASE": self.parse_table_BASE_,
|
||
|
"OS/2": self.parse_table_OS_2_,
|
||
|
"STAT": self.parse_table_STAT_,
|
||
|
}.get(name)
|
||
|
if handler:
|
||
|
handler(table)
|
||
|
else:
|
||
|
raise FeatureLibError(
|
||
|
'"table %s" is not supported' % name.strip(), location
|
||
|
)
|
||
|
self.expect_symbol_("}")
|
||
|
end_tag = self.expect_tag_()
|
||
|
if end_tag != name:
|
||
|
raise FeatureLibError(
|
||
|
'Expected "%s"' % name.strip(), self.cur_token_location_
|
||
|
)
|
||
|
self.expect_symbol_(";")
|
||
|
return table
|
||
|
|
||
|
def parse_table_GDEF_(self, table):
|
||
|
statements = table.statements
|
||
|
while self.next_token_ != "}" or self.cur_comments_:
|
||
|
self.advance_lexer_(comments=True)
|
||
|
if self.cur_token_type_ is Lexer.COMMENT:
|
||
|
statements.append(
|
||
|
self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
|
||
|
)
|
||
|
elif self.is_cur_keyword_("Attach"):
|
||
|
statements.append(self.parse_attach_())
|
||
|
elif self.is_cur_keyword_("GlyphClassDef"):
|
||
|
statements.append(self.parse_GlyphClassDef_())
|
||
|
elif self.is_cur_keyword_("LigatureCaretByIndex"):
|
||
|
statements.append(self.parse_ligatureCaretByIndex_())
|
||
|
elif self.is_cur_keyword_("LigatureCaretByPos"):
|
||
|
statements.append(self.parse_ligatureCaretByPos_())
|
||
|
elif self.cur_token_ == ";":
|
||
|
continue
|
||
|
else:
|
||
|
raise FeatureLibError(
|
||
|
"Expected Attach, LigatureCaretByIndex, " "or LigatureCaretByPos",
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
|
||
|
def parse_table_head_(self, table):
|
||
|
statements = table.statements
|
||
|
while self.next_token_ != "}" or self.cur_comments_:
|
||
|
self.advance_lexer_(comments=True)
|
||
|
if self.cur_token_type_ is Lexer.COMMENT:
|
||
|
statements.append(
|
||
|
self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
|
||
|
)
|
||
|
elif self.is_cur_keyword_("FontRevision"):
|
||
|
statements.append(self.parse_FontRevision_())
|
||
|
elif self.cur_token_ == ";":
|
||
|
continue
|
||
|
else:
|
||
|
raise FeatureLibError("Expected FontRevision", self.cur_token_location_)
|
||
|
|
||
|
def parse_table_hhea_(self, table):
|
||
|
statements = table.statements
|
||
|
fields = ("CaretOffset", "Ascender", "Descender", "LineGap")
|
||
|
while self.next_token_ != "}" or self.cur_comments_:
|
||
|
self.advance_lexer_(comments=True)
|
||
|
if self.cur_token_type_ is Lexer.COMMENT:
|
||
|
statements.append(
|
||
|
self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
|
||
|
)
|
||
|
elif self.cur_token_type_ is Lexer.NAME and self.cur_token_ in fields:
|
||
|
key = self.cur_token_.lower()
|
||
|
value = self.expect_number_()
|
||
|
statements.append(
|
||
|
self.ast.HheaField(key, value, location=self.cur_token_location_)
|
||
|
)
|
||
|
if self.next_token_ != ";":
|
||
|
raise FeatureLibError(
|
||
|
"Incomplete statement", self.next_token_location_
|
||
|
)
|
||
|
elif self.cur_token_ == ";":
|
||
|
continue
|
||
|
else:
|
||
|
raise FeatureLibError(
|
||
|
"Expected CaretOffset, Ascender, " "Descender or LineGap",
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
|
||
|
def parse_table_vhea_(self, table):
|
||
|
statements = table.statements
|
||
|
fields = ("VertTypoAscender", "VertTypoDescender", "VertTypoLineGap")
|
||
|
while self.next_token_ != "}" or self.cur_comments_:
|
||
|
self.advance_lexer_(comments=True)
|
||
|
if self.cur_token_type_ is Lexer.COMMENT:
|
||
|
statements.append(
|
||
|
self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
|
||
|
)
|
||
|
elif self.cur_token_type_ is Lexer.NAME and self.cur_token_ in fields:
|
||
|
key = self.cur_token_.lower()
|
||
|
value = self.expect_number_()
|
||
|
statements.append(
|
||
|
self.ast.VheaField(key, value, location=self.cur_token_location_)
|
||
|
)
|
||
|
if self.next_token_ != ";":
|
||
|
raise FeatureLibError(
|
||
|
"Incomplete statement", self.next_token_location_
|
||
|
)
|
||
|
elif self.cur_token_ == ";":
|
||
|
continue
|
||
|
else:
|
||
|
raise FeatureLibError(
|
||
|
"Expected VertTypoAscender, "
|
||
|
"VertTypoDescender or VertTypoLineGap",
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
|
||
|
def parse_table_name_(self, table):
|
||
|
statements = table.statements
|
||
|
while self.next_token_ != "}" or self.cur_comments_:
|
||
|
self.advance_lexer_(comments=True)
|
||
|
if self.cur_token_type_ is Lexer.COMMENT:
|
||
|
statements.append(
|
||
|
self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
|
||
|
)
|
||
|
elif self.is_cur_keyword_("nameid"):
|
||
|
statement = self.parse_nameid_()
|
||
|
if statement:
|
||
|
statements.append(statement)
|
||
|
elif self.cur_token_ == ";":
|
||
|
continue
|
||
|
else:
|
||
|
raise FeatureLibError("Expected nameid", self.cur_token_location_)
|
||
|
|
||
|
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
|
||
|
langID = None
|
||
|
if self.next_token_type_ in Lexer.NUMBERS:
|
||
|
platformID = self.expect_any_number_()
|
||
|
location = self.cur_token_location_
|
||
|
if platformID not in (1, 3):
|
||
|
raise FeatureLibError("Expected platform id 1 or 3", location)
|
||
|
if self.next_token_type_ in Lexer.NUMBERS:
|
||
|
platEncID = self.expect_any_number_()
|
||
|
langID = self.expect_any_number_()
|
||
|
else:
|
||
|
platformID = 3
|
||
|
location = self.cur_token_location_
|
||
|
|
||
|
if platformID == 1: # Macintosh
|
||
|
platEncID = platEncID or 0 # Roman
|
||
|
langID = langID or 0 # English
|
||
|
else: # 3, Windows
|
||
|
platEncID = platEncID or 1 # Unicode
|
||
|
langID = langID or 0x0409 # English
|
||
|
|
||
|
string = self.expect_string_()
|
||
|
self.expect_symbol_(";")
|
||
|
|
||
|
encoding = getEncoding(platformID, platEncID, langID)
|
||
|
if encoding is None:
|
||
|
raise FeatureLibError("Unsupported encoding", location)
|
||
|
unescaped = self.unescape_string_(string, encoding)
|
||
|
return platformID, platEncID, langID, unescaped
|
||
|
|
||
|
def parse_stat_name_(self):
|
||
|
platEncID = None
|
||
|
langID = None
|
||
|
if self.next_token_type_ in Lexer.NUMBERS:
|
||
|
platformID = self.expect_any_number_()
|
||
|
location = self.cur_token_location_
|
||
|
if platformID not in (1, 3):
|
||
|
raise FeatureLibError("Expected platform id 1 or 3", location)
|
||
|
if self.next_token_type_ in Lexer.NUMBERS:
|
||
|
platEncID = self.expect_any_number_()
|
||
|
langID = self.expect_any_number_()
|
||
|
else:
|
||
|
platformID = 3
|
||
|
location = self.cur_token_location_
|
||
|
|
||
|
if platformID == 1: # Macintosh
|
||
|
platEncID = platEncID or 0 # Roman
|
||
|
langID = langID or 0 # English
|
||
|
else: # 3, Windows
|
||
|
platEncID = platEncID or 1 # Unicode
|
||
|
langID = langID or 0x0409 # English
|
||
|
|
||
|
string = self.expect_string_()
|
||
|
encoding = getEncoding(platformID, platEncID, langID)
|
||
|
if encoding is None:
|
||
|
raise FeatureLibError("Unsupported encoding", location)
|
||
|
unescaped = self.unescape_string_(string, encoding)
|
||
|
return platformID, platEncID, langID, unescaped
|
||
|
|
||
|
def parse_nameid_(self):
|
||
|
assert self.cur_token_ == "nameid", self.cur_token_
|
||
|
location, nameID = self.cur_token_location_, self.expect_any_number_()
|
||
|
if nameID > 32767:
|
||
|
raise FeatureLibError(
|
||
|
"Name id value cannot be greater than 32767", self.cur_token_location_
|
||
|
)
|
||
|
platformID, platEncID, langID, string = self.parse_name_()
|
||
|
return self.ast.NameRecord(
|
||
|
nameID, platformID, platEncID, langID, string, location=location
|
||
|
)
|
||
|
|
||
|
def unescape_string_(self, string, encoding):
|
||
|
if encoding == "utf_16_be":
|
||
|
s = re.sub(r"\\[0-9a-fA-F]{4}", self.unescape_unichr_, string)
|
||
|
else:
|
||
|
unescape = lambda m: self.unescape_byte_(m, encoding)
|
||
|
s = re.sub(r"\\[0-9a-fA-F]{2}", unescape, string)
|
||
|
# We now have a Unicode string, but it might contain surrogate pairs.
|
||
|
# We convert surrogates to actual Unicode by round-tripping through
|
||
|
# Python's UTF-16 codec in a special mode.
|
||
|
utf16 = tobytes(s, "utf_16_be", "surrogatepass")
|
||
|
return tostr(utf16, "utf_16_be")
|
||
|
|
||
|
@staticmethod
|
||
|
def unescape_unichr_(match):
|
||
|
n = match.group(0)[1:]
|
||
|
return chr(int(n, 16))
|
||
|
|
||
|
@staticmethod
|
||
|
def unescape_byte_(match, encoding):
|
||
|
n = match.group(0)[1:]
|
||
|
return bytechr(int(n, 16)).decode(encoding)
|
||
|
|
||
|
def parse_table_BASE_(self, table):
|
||
|
statements = table.statements
|
||
|
while self.next_token_ != "}" or self.cur_comments_:
|
||
|
self.advance_lexer_(comments=True)
|
||
|
if self.cur_token_type_ is Lexer.COMMENT:
|
||
|
statements.append(
|
||
|
self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
|
||
|
)
|
||
|
elif self.is_cur_keyword_("HorizAxis.BaseTagList"):
|
||
|
horiz_bases = self.parse_base_tag_list_()
|
||
|
elif self.is_cur_keyword_("HorizAxis.BaseScriptList"):
|
||
|
horiz_scripts = self.parse_base_script_list_(len(horiz_bases))
|
||
|
statements.append(
|
||
|
self.ast.BaseAxis(
|
||
|
horiz_bases,
|
||
|
horiz_scripts,
|
||
|
False,
|
||
|
location=self.cur_token_location_,
|
||
|
)
|
||
|
)
|
||
|
elif self.is_cur_keyword_("VertAxis.BaseTagList"):
|
||
|
vert_bases = self.parse_base_tag_list_()
|
||
|
elif self.is_cur_keyword_("VertAxis.BaseScriptList"):
|
||
|
vert_scripts = self.parse_base_script_list_(len(vert_bases))
|
||
|
statements.append(
|
||
|
self.ast.BaseAxis(
|
||
|
vert_bases,
|
||
|
vert_scripts,
|
||
|
True,
|
||
|
location=self.cur_token_location_,
|
||
|
)
|
||
|
)
|
||
|
elif self.cur_token_ == ";":
|
||
|
continue
|
||
|
|
||
|
def parse_table_OS_2_(self, table):
|
||
|
statements = table.statements
|
||
|
numbers = (
|
||
|
"FSType",
|
||
|
"TypoAscender",
|
||
|
"TypoDescender",
|
||
|
"TypoLineGap",
|
||
|
"winAscent",
|
||
|
"winDescent",
|
||
|
"XHeight",
|
||
|
"CapHeight",
|
||
|
"WeightClass",
|
||
|
"WidthClass",
|
||
|
"LowerOpSize",
|
||
|
"UpperOpSize",
|
||
|
)
|
||
|
ranges = ("UnicodeRange", "CodePageRange")
|
||
|
while self.next_token_ != "}" or self.cur_comments_:
|
||
|
self.advance_lexer_(comments=True)
|
||
|
if self.cur_token_type_ is Lexer.COMMENT:
|
||
|
statements.append(
|
||
|
self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
|
||
|
)
|
||
|
elif self.cur_token_type_ is Lexer.NAME:
|
||
|
key = self.cur_token_.lower()
|
||
|
value = None
|
||
|
if self.cur_token_ in numbers:
|
||
|
value = self.expect_number_()
|
||
|
elif self.is_cur_keyword_("Panose"):
|
||
|
value = []
|
||
|
for i in range(10):
|
||
|
value.append(self.expect_number_())
|
||
|
elif self.cur_token_ in ranges:
|
||
|
value = []
|
||
|
while self.next_token_ != ";":
|
||
|
value.append(self.expect_number_())
|
||
|
elif self.is_cur_keyword_("Vendor"):
|
||
|
value = self.expect_string_()
|
||
|
statements.append(
|
||
|
self.ast.OS2Field(key, value, location=self.cur_token_location_)
|
||
|
)
|
||
|
elif self.cur_token_ == ";":
|
||
|
continue
|
||
|
|
||
|
def parse_STAT_ElidedFallbackName(self):
|
||
|
assert self.is_cur_keyword_("ElidedFallbackName")
|
||
|
self.expect_symbol_("{")
|
||
|
names = []
|
||
|
while self.next_token_ != "}" or self.cur_comments_:
|
||
|
self.advance_lexer_()
|
||
|
if self.is_cur_keyword_("name"):
|
||
|
platformID, platEncID, langID, string = self.parse_stat_name_()
|
||
|
nameRecord = self.ast.STATNameStatement(
|
||
|
"stat",
|
||
|
platformID,
|
||
|
platEncID,
|
||
|
langID,
|
||
|
string,
|
||
|
location=self.cur_token_location_,
|
||
|
)
|
||
|
names.append(nameRecord)
|
||
|
else:
|
||
|
if self.cur_token_ != ";":
|
||
|
raise FeatureLibError(
|
||
|
f"Unexpected token {self.cur_token_} " f"in ElidedFallbackName",
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
self.expect_symbol_("}")
|
||
|
if not names:
|
||
|
raise FeatureLibError('Expected "name"', self.cur_token_location_)
|
||
|
return names
|
||
|
|
||
|
def parse_STAT_design_axis(self):
|
||
|
assert self.is_cur_keyword_("DesignAxis")
|
||
|
names = []
|
||
|
axisTag = self.expect_tag_()
|
||
|
if (
|
||
|
axisTag not in ("ital", "opsz", "slnt", "wdth", "wght")
|
||
|
and not axisTag.isupper()
|
||
|
):
|
||
|
log.warning(f"Unregistered axis tag {axisTag} should be uppercase.")
|
||
|
axisOrder = self.expect_number_()
|
||
|
self.expect_symbol_("{")
|
||
|
while self.next_token_ != "}" or self.cur_comments_:
|
||
|
self.advance_lexer_()
|
||
|
if self.cur_token_type_ is Lexer.COMMENT:
|
||
|
continue
|
||
|
elif self.is_cur_keyword_("name"):
|
||
|
location = self.cur_token_location_
|
||
|
platformID, platEncID, langID, string = self.parse_stat_name_()
|
||
|
name = self.ast.STATNameStatement(
|
||
|
"stat", platformID, platEncID, langID, string, location=location
|
||
|
)
|
||
|
names.append(name)
|
||
|
elif self.cur_token_ == ";":
|
||
|
continue
|
||
|
else:
|
||
|
raise FeatureLibError(
|
||
|
f'Expected "name", got {self.cur_token_}', self.cur_token_location_
|
||
|
)
|
||
|
|
||
|
self.expect_symbol_("}")
|
||
|
return self.ast.STATDesignAxisStatement(
|
||
|
axisTag, axisOrder, names, self.cur_token_location_
|
||
|
)
|
||
|
|
||
|
def parse_STAT_axis_value_(self):
|
||
|
assert self.is_cur_keyword_("AxisValue")
|
||
|
self.expect_symbol_("{")
|
||
|
locations = []
|
||
|
names = []
|
||
|
flags = 0
|
||
|
while self.next_token_ != "}" or self.cur_comments_:
|
||
|
self.advance_lexer_(comments=True)
|
||
|
if self.cur_token_type_ is Lexer.COMMENT:
|
||
|
continue
|
||
|
elif self.is_cur_keyword_("name"):
|
||
|
location = self.cur_token_location_
|
||
|
platformID, platEncID, langID, string = self.parse_stat_name_()
|
||
|
name = self.ast.STATNameStatement(
|
||
|
"stat", platformID, platEncID, langID, string, location=location
|
||
|
)
|
||
|
names.append(name)
|
||
|
elif self.is_cur_keyword_("location"):
|
||
|
location = self.parse_STAT_location()
|
||
|
locations.append(location)
|
||
|
elif self.is_cur_keyword_("flag"):
|
||
|
flags = self.expect_stat_flags()
|
||
|
elif self.cur_token_ == ";":
|
||
|
continue
|
||
|
else:
|
||
|
raise FeatureLibError(
|
||
|
f"Unexpected token {self.cur_token_} " f"in AxisValue",
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
self.expect_symbol_("}")
|
||
|
if not names:
|
||
|
raise FeatureLibError('Expected "Axis Name"', self.cur_token_location_)
|
||
|
if not locations:
|
||
|
raise FeatureLibError('Expected "Axis location"', self.cur_token_location_)
|
||
|
if len(locations) > 1:
|
||
|
for location in locations:
|
||
|
if len(location.values) > 1:
|
||
|
raise FeatureLibError(
|
||
|
"Only one value is allowed in a "
|
||
|
"Format 4 Axis Value Record, but "
|
||
|
f"{len(location.values)} were found.",
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
format4_tags = []
|
||
|
for location in locations:
|
||
|
tag = location.tag
|
||
|
if tag in format4_tags:
|
||
|
raise FeatureLibError(
|
||
|
f"Axis tag {tag} already " "defined.", self.cur_token_location_
|
||
|
)
|
||
|
format4_tags.append(tag)
|
||
|
|
||
|
return self.ast.STATAxisValueStatement(
|
||
|
names, locations, flags, self.cur_token_location_
|
||
|
)
|
||
|
|
||
|
def parse_STAT_location(self):
|
||
|
values = []
|
||
|
tag = self.expect_tag_()
|
||
|
if len(tag.strip()) != 4:
|
||
|
raise FeatureLibError(
|
||
|
f"Axis tag {self.cur_token_} must be 4 " "characters",
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
|
||
|
while self.next_token_ != ";":
|
||
|
if self.next_token_type_ is Lexer.FLOAT:
|
||
|
value = self.expect_float_()
|
||
|
values.append(value)
|
||
|
elif self.next_token_type_ is Lexer.NUMBER:
|
||
|
value = self.expect_number_()
|
||
|
values.append(value)
|
||
|
else:
|
||
|
raise FeatureLibError(
|
||
|
f'Unexpected value "{self.next_token_}". '
|
||
|
"Expected integer or float.",
|
||
|
self.next_token_location_,
|
||
|
)
|
||
|
if len(values) == 3:
|
||
|
nominal, min_val, max_val = values
|
||
|
if nominal < min_val or nominal > max_val:
|
||
|
raise FeatureLibError(
|
||
|
f"Default value {nominal} is outside "
|
||
|
f"of specified range "
|
||
|
f"{min_val}-{max_val}.",
|
||
|
self.next_token_location_,
|
||
|
)
|
||
|
return self.ast.AxisValueLocationStatement(tag, values)
|
||
|
|
||
|
def parse_table_STAT_(self, table):
|
||
|
statements = table.statements
|
||
|
design_axes = []
|
||
|
while self.next_token_ != "}" or self.cur_comments_:
|
||
|
self.advance_lexer_(comments=True)
|
||
|
if self.cur_token_type_ is Lexer.COMMENT:
|
||
|
statements.append(
|
||
|
self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
|
||
|
)
|
||
|
elif self.cur_token_type_ is Lexer.NAME:
|
||
|
if self.is_cur_keyword_("ElidedFallbackName"):
|
||
|
names = self.parse_STAT_ElidedFallbackName()
|
||
|
statements.append(self.ast.ElidedFallbackName(names))
|
||
|
elif self.is_cur_keyword_("ElidedFallbackNameID"):
|
||
|
value = self.expect_number_()
|
||
|
statements.append(self.ast.ElidedFallbackNameID(value))
|
||
|
self.expect_symbol_(";")
|
||
|
elif self.is_cur_keyword_("DesignAxis"):
|
||
|
designAxis = self.parse_STAT_design_axis()
|
||
|
design_axes.append(designAxis.tag)
|
||
|
statements.append(designAxis)
|
||
|
self.expect_symbol_(";")
|
||
|
elif self.is_cur_keyword_("AxisValue"):
|
||
|
axisValueRecord = self.parse_STAT_axis_value_()
|
||
|
for location in axisValueRecord.locations:
|
||
|
if location.tag not in design_axes:
|
||
|
# Tag must be defined in a DesignAxis before it
|
||
|
# can be referenced
|
||
|
raise FeatureLibError(
|
||
|
"DesignAxis not defined for " f"{location.tag}.",
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
statements.append(axisValueRecord)
|
||
|
self.expect_symbol_(";")
|
||
|
else:
|
||
|
raise FeatureLibError(
|
||
|
f"Unexpected token {self.cur_token_}", self.cur_token_location_
|
||
|
)
|
||
|
elif self.cur_token_ == ";":
|
||
|
continue
|
||
|
|
||
|
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",
|
||
|
"VertAxis.BaseTagList",
|
||
|
), self.cur_token_
|
||
|
bases = []
|
||
|
while self.next_token_ != ";":
|
||
|
bases.append(self.expect_script_tag_())
|
||
|
self.expect_symbol_(";")
|
||
|
return bases
|
||
|
|
||
|
def parse_base_script_list_(self, count):
|
||
|
assert self.cur_token_ in (
|
||
|
"HorizAxis.BaseScriptList",
|
||
|
"VertAxis.BaseScriptList",
|
||
|
), self.cur_token_
|
||
|
scripts = [(self.parse_base_script_record_(count))]
|
||
|
while self.next_token_ == ",":
|
||
|
self.expect_symbol_(",")
|
||
|
scripts.append(self.parse_base_script_record_(count))
|
||
|
self.expect_symbol_(";")
|
||
|
return scripts
|
||
|
|
||
|
def parse_base_script_record_(self, count):
|
||
|
script_tag = self.expect_script_tag_()
|
||
|
base_tag = self.expect_script_tag_()
|
||
|
coords = [self.expect_number_() for i in range(count)]
|
||
|
return script_tag, base_tag, coords
|
||
|
|
||
|
def parse_device_(self):
|
||
|
result = None
|
||
|
self.expect_symbol_("<")
|
||
|
self.expect_keyword_("device")
|
||
|
if self.next_token_ == "NULL":
|
||
|
self.expect_keyword_("NULL")
|
||
|
else:
|
||
|
result = [(self.expect_number_(), self.expect_number_())]
|
||
|
while self.next_token_ == ",":
|
||
|
self.expect_symbol_(",")
|
||
|
result.append((self.expect_number_(), self.expect_number_()))
|
||
|
result = tuple(result) # make it hashable
|
||
|
self.expect_symbol_(">")
|
||
|
return result
|
||
|
|
||
|
def is_next_value_(self):
|
||
|
return (
|
||
|
self.next_token_type_ is Lexer.NUMBER
|
||
|
or self.next_token_ == "<"
|
||
|
or self.next_token_ == "("
|
||
|
)
|
||
|
|
||
|
def parse_valuerecord_(self, vertical):
|
||
|
if (
|
||
|
self.next_token_type_ is Lexer.SYMBOL and self.next_token_ == "("
|
||
|
) or self.next_token_type_ is Lexer.NUMBER:
|
||
|
number, location = (
|
||
|
self.expect_number_(variable=True),
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
if vertical:
|
||
|
val = self.ast.ValueRecord(
|
||
|
yAdvance=number, vertical=vertical, location=location
|
||
|
)
|
||
|
else:
|
||
|
val = self.ast.ValueRecord(
|
||
|
xAdvance=number, vertical=vertical, location=location
|
||
|
)
|
||
|
return val
|
||
|
self.expect_symbol_("<")
|
||
|
location = self.cur_token_location_
|
||
|
if self.next_token_type_ is Lexer.NAME:
|
||
|
name = self.expect_name_()
|
||
|
if name == "NULL":
|
||
|
self.expect_symbol_(">")
|
||
|
return self.ast.ValueRecord()
|
||
|
vrd = self.valuerecords_.resolve(name)
|
||
|
if vrd is None:
|
||
|
raise FeatureLibError(
|
||
|
'Unknown valueRecordDef "%s"' % name, self.cur_token_location_
|
||
|
)
|
||
|
value = vrd.value
|
||
|
xPlacement, yPlacement = (value.xPlacement, value.yPlacement)
|
||
|
xAdvance, yAdvance = (value.xAdvance, value.yAdvance)
|
||
|
else:
|
||
|
xPlacement, yPlacement, xAdvance, yAdvance = (
|
||
|
self.expect_number_(variable=True),
|
||
|
self.expect_number_(variable=True),
|
||
|
self.expect_number_(variable=True),
|
||
|
self.expect_number_(variable=True),
|
||
|
)
|
||
|
|
||
|
if self.next_token_ == "<":
|
||
|
xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice = (
|
||
|
self.parse_device_(),
|
||
|
self.parse_device_(),
|
||
|
self.parse_device_(),
|
||
|
self.parse_device_(),
|
||
|
)
|
||
|
allDeltas = sorted(
|
||
|
[
|
||
|
delta
|
||
|
for size, delta in (xPlaDevice if xPlaDevice else ())
|
||
|
+ (yPlaDevice if yPlaDevice else ())
|
||
|
+ (xAdvDevice if xAdvDevice else ())
|
||
|
+ (yAdvDevice if yAdvDevice else ())
|
||
|
]
|
||
|
)
|
||
|
if allDeltas[0] < -128 or allDeltas[-1] > 127:
|
||
|
raise FeatureLibError(
|
||
|
"Device value out of valid range (-128..127)",
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
else:
|
||
|
xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice = (None, None, None, None)
|
||
|
|
||
|
self.expect_symbol_(">")
|
||
|
return self.ast.ValueRecord(
|
||
|
xPlacement,
|
||
|
yPlacement,
|
||
|
xAdvance,
|
||
|
yAdvance,
|
||
|
xPlaDevice,
|
||
|
yPlaDevice,
|
||
|
xAdvDevice,
|
||
|
yAdvDevice,
|
||
|
vertical=vertical,
|
||
|
location=location,
|
||
|
)
|
||
|
|
||
|
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")
|
||
|
location = self.cur_token_location_
|
||
|
value = self.parse_valuerecord_(vertical)
|
||
|
name = self.expect_name_()
|
||
|
self.expect_symbol_(";")
|
||
|
vrd = self.ast.ValueRecordDefinition(name, value, location=location)
|
||
|
self.valuerecords_.define(name, vrd)
|
||
|
return vrd
|
||
|
|
||
|
def parse_languagesystem_(self):
|
||
|
assert self.cur_token_ == "languagesystem"
|
||
|
location = self.cur_token_location_
|
||
|
script = self.expect_script_tag_()
|
||
|
language = self.expect_language_tag_()
|
||
|
self.expect_symbol_(";")
|
||
|
return self.ast.LanguageSystemStatement(script, language, location=location)
|
||
|
|
||
|
def parse_feature_block_(self, variation=False):
|
||
|
if variation:
|
||
|
assert self.cur_token_ == "variation"
|
||
|
else:
|
||
|
assert self.cur_token_ == "feature"
|
||
|
location = self.cur_token_location_
|
||
|
tag = self.expect_tag_()
|
||
|
vertical = tag in {"vkrn", "vpal", "vhal", "valt"}
|
||
|
|
||
|
stylisticset = None
|
||
|
cv_feature = None
|
||
|
size_feature = False
|
||
|
if tag in self.SS_FEATURE_TAGS:
|
||
|
stylisticset = tag
|
||
|
elif tag in self.CV_FEATURE_TAGS:
|
||
|
cv_feature = tag
|
||
|
elif tag == "size":
|
||
|
size_feature = True
|
||
|
|
||
|
if variation:
|
||
|
conditionset = self.expect_name_()
|
||
|
|
||
|
use_extension = False
|
||
|
if self.next_token_ == "useExtension":
|
||
|
self.expect_keyword_("useExtension")
|
||
|
use_extension = True
|
||
|
|
||
|
if variation:
|
||
|
block = self.ast.VariationBlock(
|
||
|
tag, conditionset, use_extension=use_extension, location=location
|
||
|
)
|
||
|
else:
|
||
|
block = self.ast.FeatureBlock(
|
||
|
tag, use_extension=use_extension, location=location
|
||
|
)
|
||
|
self.parse_block_(block, vertical, stylisticset, size_feature, cv_feature)
|
||
|
return block
|
||
|
|
||
|
def parse_feature_reference_(self):
|
||
|
assert self.cur_token_ == "feature", self.cur_token_
|
||
|
location = self.cur_token_location_
|
||
|
featureName = self.expect_tag_()
|
||
|
self.expect_symbol_(";")
|
||
|
return self.ast.FeatureReferenceStatement(featureName, location=location)
|
||
|
|
||
|
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_
|
||
|
block = self.ast.NestedBlock(
|
||
|
tag, self.cur_token_, location=self.cur_token_location_
|
||
|
)
|
||
|
self.expect_symbol_("{")
|
||
|
for symtab in self.symbol_tables_:
|
||
|
symtab.enter_scope()
|
||
|
while self.next_token_ != "}" or self.cur_comments_:
|
||
|
self.advance_lexer_(comments=True)
|
||
|
if self.cur_token_type_ is Lexer.COMMENT:
|
||
|
block.statements.append(
|
||
|
self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
|
||
|
)
|
||
|
elif self.is_cur_keyword_("name"):
|
||
|
location = self.cur_token_location_
|
||
|
platformID, platEncID, langID, string = self.parse_name_()
|
||
|
block.statements.append(
|
||
|
self.ast.FeatureNameStatement(
|
||
|
tag, platformID, platEncID, langID, string, location=location
|
||
|
)
|
||
|
)
|
||
|
elif self.cur_token_ == ";":
|
||
|
continue
|
||
|
else:
|
||
|
raise FeatureLibError('Expected "name"', self.cur_token_location_)
|
||
|
self.expect_symbol_("}")
|
||
|
for symtab in self.symbol_tables_:
|
||
|
symtab.exit_scope()
|
||
|
self.expect_symbol_(";")
|
||
|
return block
|
||
|
|
||
|
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_
|
||
|
block = self.ast.NestedBlock(
|
||
|
tag, self.cur_token_, location=self.cur_token_location_
|
||
|
)
|
||
|
self.expect_symbol_("{")
|
||
|
for symtab in self.symbol_tables_:
|
||
|
symtab.enter_scope()
|
||
|
|
||
|
statements = block.statements
|
||
|
while self.next_token_ != "}" or self.cur_comments_:
|
||
|
self.advance_lexer_(comments=True)
|
||
|
if self.cur_token_type_ is Lexer.COMMENT:
|
||
|
statements.append(
|
||
|
self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
|
||
|
)
|
||
|
elif self.is_cur_keyword_(
|
||
|
{
|
||
|
"FeatUILabelNameID",
|
||
|
"FeatUITooltipTextNameID",
|
||
|
"SampleTextNameID",
|
||
|
"ParamUILabelNameID",
|
||
|
}
|
||
|
):
|
||
|
statements.append(self.parse_cvNameIDs_(tag, self.cur_token_))
|
||
|
elif self.is_cur_keyword_("Character"):
|
||
|
statements.append(self.parse_cvCharacter_(tag))
|
||
|
elif self.cur_token_ == ";":
|
||
|
continue
|
||
|
else:
|
||
|
raise FeatureLibError(
|
||
|
"Expected statement: got {} {}".format(
|
||
|
self.cur_token_type_, self.cur_token_
|
||
|
),
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
|
||
|
self.expect_symbol_("}")
|
||
|
for symtab in self.symbol_tables_:
|
||
|
symtab.exit_scope()
|
||
|
self.expect_symbol_(";")
|
||
|
return block
|
||
|
|
||
|
def parse_cvNameIDs_(self, tag, block_name):
|
||
|
assert self.cur_token_ == block_name, self.cur_token_
|
||
|
block = self.ast.NestedBlock(tag, block_name, location=self.cur_token_location_)
|
||
|
self.expect_symbol_("{")
|
||
|
for symtab in self.symbol_tables_:
|
||
|
symtab.enter_scope()
|
||
|
while self.next_token_ != "}" or self.cur_comments_:
|
||
|
self.advance_lexer_(comments=True)
|
||
|
if self.cur_token_type_ is Lexer.COMMENT:
|
||
|
block.statements.append(
|
||
|
self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
|
||
|
)
|
||
|
elif self.is_cur_keyword_("name"):
|
||
|
location = self.cur_token_location_
|
||
|
platformID, platEncID, langID, string = self.parse_name_()
|
||
|
block.statements.append(
|
||
|
self.ast.CVParametersNameStatement(
|
||
|
tag,
|
||
|
platformID,
|
||
|
platEncID,
|
||
|
langID,
|
||
|
string,
|
||
|
block_name,
|
||
|
location=location,
|
||
|
)
|
||
|
)
|
||
|
elif self.cur_token_ == ";":
|
||
|
continue
|
||
|
else:
|
||
|
raise FeatureLibError('Expected "name"', self.cur_token_location_)
|
||
|
self.expect_symbol_("}")
|
||
|
for symtab in self.symbol_tables_:
|
||
|
symtab.exit_scope()
|
||
|
self.expect_symbol_(";")
|
||
|
return block
|
||
|
|
||
|
def parse_cvCharacter_(self, tag):
|
||
|
assert self.cur_token_ == "Character", self.cur_token_
|
||
|
location, character = self.cur_token_location_, self.expect_any_number_()
|
||
|
self.expect_symbol_(";")
|
||
|
if not (0xFFFFFF >= character >= 0):
|
||
|
raise FeatureLibError(
|
||
|
"Character value must be between "
|
||
|
"{:#x} and {:#x}".format(0, 0xFFFFFF),
|
||
|
location,
|
||
|
)
|
||
|
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 <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.c>`_.
|
||
|
assert self.cur_token_ == "FontRevision", self.cur_token_
|
||
|
location, version = self.cur_token_location_, self.expect_float_()
|
||
|
self.expect_symbol_(";")
|
||
|
if version <= 0:
|
||
|
raise FeatureLibError("Font revision numbers must be positive", location)
|
||
|
return self.ast.FontRevisionStatement(version, location=location)
|
||
|
|
||
|
def parse_conditionset_(self):
|
||
|
name = self.expect_name_()
|
||
|
|
||
|
conditions = {}
|
||
|
self.expect_symbol_("{")
|
||
|
|
||
|
while self.next_token_ != "}":
|
||
|
self.advance_lexer_()
|
||
|
if self.cur_token_type_ is not Lexer.NAME:
|
||
|
raise FeatureLibError("Expected an axis name", self.cur_token_location_)
|
||
|
|
||
|
axis = self.cur_token_
|
||
|
if axis in conditions:
|
||
|
raise FeatureLibError(
|
||
|
f"Repeated condition for axis {axis}", self.cur_token_location_
|
||
|
)
|
||
|
|
||
|
if self.next_token_type_ is Lexer.FLOAT:
|
||
|
min_value = self.expect_float_()
|
||
|
elif self.next_token_type_ is Lexer.NUMBER:
|
||
|
min_value = self.expect_number_(variable=False)
|
||
|
|
||
|
if self.next_token_type_ is Lexer.FLOAT:
|
||
|
max_value = self.expect_float_()
|
||
|
elif self.next_token_type_ is Lexer.NUMBER:
|
||
|
max_value = self.expect_number_(variable=False)
|
||
|
self.expect_symbol_(";")
|
||
|
|
||
|
conditions[axis] = (min_value, max_value)
|
||
|
|
||
|
self.expect_symbol_("}")
|
||
|
|
||
|
finalname = self.expect_name_()
|
||
|
if finalname != name:
|
||
|
raise FeatureLibError('Expected "%s"' % name, self.cur_token_location_)
|
||
|
return self.ast.ConditionsetStatement(name, conditions)
|
||
|
|
||
|
def parse_block_(
|
||
|
self, block, vertical, stylisticset=None, size_feature=False, cv_feature=None
|
||
|
):
|
||
|
self.expect_symbol_("{")
|
||
|
for symtab in self.symbol_tables_:
|
||
|
symtab.enter_scope()
|
||
|
|
||
|
statements = block.statements
|
||
|
while self.next_token_ != "}" or self.cur_comments_:
|
||
|
self.advance_lexer_(comments=True)
|
||
|
if self.cur_token_type_ is Lexer.COMMENT:
|
||
|
statements.append(
|
||
|
self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
|
||
|
)
|
||
|
elif self.cur_token_type_ is Lexer.GLYPHCLASS:
|
||
|
statements.append(self.parse_glyphclass_definition_())
|
||
|
elif self.is_cur_keyword_("anchorDef"):
|
||
|
statements.append(self.parse_anchordef_())
|
||
|
elif self.is_cur_keyword_({"enum", "enumerate"}):
|
||
|
statements.append(self.parse_enumerate_(vertical=vertical))
|
||
|
elif self.is_cur_keyword_("feature"):
|
||
|
statements.append(self.parse_feature_reference_())
|
||
|
elif self.is_cur_keyword_("ignore"):
|
||
|
statements.append(self.parse_ignore_())
|
||
|
elif self.is_cur_keyword_("language"):
|
||
|
statements.append(self.parse_language_())
|
||
|
elif self.is_cur_keyword_("lookup"):
|
||
|
statements.append(self.parse_lookup_(vertical))
|
||
|
elif self.is_cur_keyword_("lookupflag"):
|
||
|
statements.append(self.parse_lookupflag_())
|
||
|
elif self.is_cur_keyword_("markClass"):
|
||
|
statements.append(self.parse_markClass_())
|
||
|
elif self.is_cur_keyword_({"pos", "position"}):
|
||
|
statements.append(
|
||
|
self.parse_position_(enumerated=False, vertical=vertical)
|
||
|
)
|
||
|
elif self.is_cur_keyword_("script"):
|
||
|
statements.append(self.parse_script_())
|
||
|
elif self.is_cur_keyword_({"sub", "substitute", "rsub", "reversesub"}):
|
||
|
statements.append(self.parse_substitute_())
|
||
|
elif self.is_cur_keyword_("subtable"):
|
||
|
statements.append(self.parse_subtable_())
|
||
|
elif self.is_cur_keyword_("valueRecordDef"):
|
||
|
statements.append(self.parse_valuerecord_definition_(vertical))
|
||
|
elif stylisticset and self.is_cur_keyword_("featureNames"):
|
||
|
statements.append(self.parse_featureNames_(stylisticset))
|
||
|
elif cv_feature and self.is_cur_keyword_("cvParameters"):
|
||
|
statements.append(self.parse_cvParameters_(cv_feature))
|
||
|
elif size_feature and self.is_cur_keyword_("parameters"):
|
||
|
statements.append(self.parse_size_parameters_())
|
||
|
elif size_feature and self.is_cur_keyword_("sizemenuname"):
|
||
|
statements.append(self.parse_size_menuname_())
|
||
|
elif (
|
||
|
self.cur_token_type_ is Lexer.NAME
|
||
|
and self.cur_token_ in self.extensions
|
||
|
):
|
||
|
statements.append(self.extensions[self.cur_token_](self))
|
||
|
elif self.cur_token_ == ";":
|
||
|
continue
|
||
|
else:
|
||
|
raise FeatureLibError(
|
||
|
"Expected glyph class definition or statement: got {} {}".format(
|
||
|
self.cur_token_type_, self.cur_token_
|
||
|
),
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
|
||
|
self.expect_symbol_("}")
|
||
|
for symtab in self.symbol_tables_:
|
||
|
symtab.exit_scope()
|
||
|
|
||
|
name = self.expect_name_()
|
||
|
if name != block.name.strip():
|
||
|
raise FeatureLibError(
|
||
|
'Expected "%s"' % block.name.strip(), self.cur_token_location_
|
||
|
)
|
||
|
self.expect_symbol_(";")
|
||
|
|
||
|
# A multiple substitution may have a single destination, in which case
|
||
|
# it will look just like a single substitution. So if there are both
|
||
|
# multiple and single substitutions, upgrade all the single ones to
|
||
|
# multiple substitutions.
|
||
|
|
||
|
# Check if we have a mix of non-contextual singles and multiples.
|
||
|
has_single = False
|
||
|
has_multiple = False
|
||
|
for s in statements:
|
||
|
if isinstance(s, self.ast.SingleSubstStatement):
|
||
|
has_single = not any([s.prefix, s.suffix, s.forceChain])
|
||
|
elif isinstance(s, self.ast.MultipleSubstStatement):
|
||
|
has_multiple = not any([s.prefix, s.suffix, s.forceChain])
|
||
|
|
||
|
# Upgrade all single substitutions to multiple substitutions.
|
||
|
if has_single and has_multiple:
|
||
|
statements = []
|
||
|
for s in block.statements:
|
||
|
if isinstance(s, self.ast.SingleSubstStatement):
|
||
|
glyphs = s.glyphs[0].glyphSet()
|
||
|
replacements = s.replacements[0].glyphSet()
|
||
|
if len(replacements) == 1:
|
||
|
replacements *= len(glyphs)
|
||
|
for i, glyph in enumerate(glyphs):
|
||
|
statements.append(
|
||
|
self.ast.MultipleSubstStatement(
|
||
|
s.prefix,
|
||
|
glyph,
|
||
|
s.suffix,
|
||
|
[replacements[i]],
|
||
|
s.forceChain,
|
||
|
location=s.location,
|
||
|
)
|
||
|
)
|
||
|
else:
|
||
|
statements.append(s)
|
||
|
block.statements = statements
|
||
|
|
||
|
def is_cur_keyword_(self, k):
|
||
|
if self.cur_token_type_ is Lexer.NAME:
|
||
|
if isinstance(k, type("")): # basestring is gone in Python3
|
||
|
return self.cur_token_ == k
|
||
|
else:
|
||
|
return self.cur_token_ in k
|
||
|
return False
|
||
|
|
||
|
def expect_class_name_(self):
|
||
|
self.advance_lexer_()
|
||
|
if self.cur_token_type_ is not Lexer.GLYPHCLASS:
|
||
|
raise FeatureLibError("Expected @NAME", self.cur_token_location_)
|
||
|
return self.cur_token_
|
||
|
|
||
|
def expect_cid_(self):
|
||
|
self.advance_lexer_()
|
||
|
if self.cur_token_type_ is Lexer.CID:
|
||
|
return self.cur_token_
|
||
|
raise FeatureLibError("Expected a CID", self.cur_token_location_)
|
||
|
|
||
|
def expect_filename_(self):
|
||
|
self.advance_lexer_()
|
||
|
if self.cur_token_type_ is not Lexer.FILENAME:
|
||
|
raise FeatureLibError("Expected file name", self.cur_token_location_)
|
||
|
return self.cur_token_
|
||
|
|
||
|
def expect_glyph_(self):
|
||
|
self.advance_lexer_()
|
||
|
if self.cur_token_type_ is Lexer.NAME:
|
||
|
return self.cur_token_.lstrip("\\")
|
||
|
elif self.cur_token_type_ is Lexer.CID:
|
||
|
return "cid%05d" % self.cur_token_
|
||
|
raise FeatureLibError("Expected a glyph name or CID", self.cur_token_location_)
|
||
|
|
||
|
def check_glyph_name_in_glyph_set(self, *names):
|
||
|
"""Adds a glyph name (just `start`) or glyph names of a
|
||
|
range (`start` and `end`) which are not in the glyph set
|
||
|
to the "missing list" for future error reporting.
|
||
|
|
||
|
If no glyph set is present, does nothing.
|
||
|
"""
|
||
|
if self.glyphNames_:
|
||
|
for name in names:
|
||
|
if name in self.glyphNames_:
|
||
|
continue
|
||
|
if name not in self.missing:
|
||
|
self.missing[name] = self.cur_token_location_
|
||
|
|
||
|
def expect_markClass_reference_(self):
|
||
|
name = self.expect_class_name_()
|
||
|
mc = self.glyphclasses_.resolve(name)
|
||
|
if mc is None:
|
||
|
raise FeatureLibError(
|
||
|
"Unknown markClass @%s" % name, self.cur_token_location_
|
||
|
)
|
||
|
if not isinstance(mc, self.ast.MarkClass):
|
||
|
raise FeatureLibError(
|
||
|
"@%s is not a markClass" % name, self.cur_token_location_
|
||
|
)
|
||
|
return mc
|
||
|
|
||
|
def expect_tag_(self):
|
||
|
self.advance_lexer_()
|
||
|
if self.cur_token_type_ is not Lexer.NAME:
|
||
|
raise FeatureLibError("Expected a tag", self.cur_token_location_)
|
||
|
if len(self.cur_token_) > 4:
|
||
|
raise FeatureLibError(
|
||
|
"Tags cannot be longer than 4 characters", self.cur_token_location_
|
||
|
)
|
||
|
return (self.cur_token_ + " ")[:4]
|
||
|
|
||
|
def expect_script_tag_(self):
|
||
|
tag = self.expect_tag_()
|
||
|
if tag == "dflt":
|
||
|
raise FeatureLibError(
|
||
|
'"dflt" is not a valid script tag; use "DFLT" instead',
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
return tag
|
||
|
|
||
|
def expect_language_tag_(self):
|
||
|
tag = self.expect_tag_()
|
||
|
if tag == "DFLT":
|
||
|
raise FeatureLibError(
|
||
|
'"DFLT" is not a valid language tag; use "dflt" instead',
|
||
|
self.cur_token_location_,
|
||
|
)
|
||
|
return tag
|
||
|
|
||
|
def expect_symbol_(self, symbol):
|
||
|
self.advance_lexer_()
|
||
|
if self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == symbol:
|
||
|
return symbol
|
||
|
raise FeatureLibError("Expected '%s'" % symbol, self.cur_token_location_)
|
||
|
|
||
|
def expect_keyword_(self, keyword):
|
||
|
self.advance_lexer_()
|
||
|
if self.cur_token_type_ is Lexer.NAME and self.cur_token_ == keyword:
|
||
|
return self.cur_token_
|
||
|
raise FeatureLibError('Expected "%s"' % keyword, self.cur_token_location_)
|
||
|
|
||
|
def expect_name_(self):
|
||
|
self.advance_lexer_()
|
||
|
if self.cur_token_type_ is Lexer.NAME:
|
||
|
return self.cur_token_
|
||
|
raise FeatureLibError("Expected a name", self.cur_token_location_)
|
||
|
|
||
|
def expect_number_(self, variable=False):
|
||
|
self.advance_lexer_()
|
||
|
if self.cur_token_type_ is Lexer.NUMBER:
|
||
|
return self.cur_token_
|
||
|
if variable and self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == "(":
|
||
|
return self.expect_variable_scalar_()
|
||
|
raise FeatureLibError("Expected a number", self.cur_token_location_)
|
||
|
|
||
|
def expect_variable_scalar_(self):
|
||
|
self.advance_lexer_() # "("
|
||
|
scalar = VariableScalar()
|
||
|
while True:
|
||
|
if self.cur_token_type_ == Lexer.SYMBOL and self.cur_token_ == ")":
|
||
|
break
|
||
|
location, value = self.expect_master_()
|
||
|
scalar.add_value(location, value)
|
||
|
return scalar
|
||
|
|
||
|
def expect_master_(self):
|
||
|
location = {}
|
||
|
while True:
|
||
|
if self.cur_token_type_ is not Lexer.NAME:
|
||
|
raise FeatureLibError("Expected an axis name", self.cur_token_location_)
|
||
|
axis = self.cur_token_
|
||
|
self.advance_lexer_()
|
||
|
if not (self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == "="):
|
||
|
raise FeatureLibError(
|
||
|
"Expected an equals sign", self.cur_token_location_
|
||
|
)
|
||
|
value = self.expect_number_()
|
||
|
location[axis] = value
|
||
|
if self.next_token_type_ is Lexer.NAME and self.next_token_[0] == ":":
|
||
|
# Lexer has just read the value as a glyph name. We'll correct it later
|
||
|
break
|
||
|
self.advance_lexer_()
|
||
|
if not (self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == ","):
|
||
|
raise FeatureLibError(
|
||
|
"Expected an comma or an equals sign", self.cur_token_location_
|
||
|
)
|
||
|
self.advance_lexer_()
|
||
|
self.advance_lexer_()
|
||
|
value = int(self.cur_token_[1:])
|
||
|
self.advance_lexer_()
|
||
|
return location, value
|
||
|
|
||
|
def expect_any_number_(self):
|
||
|
self.advance_lexer_()
|
||
|
if self.cur_token_type_ in Lexer.NUMBERS:
|
||
|
return self.cur_token_
|
||
|
raise FeatureLibError(
|
||
|
"Expected a decimal, hexadecimal or octal number", self.cur_token_location_
|
||
|
)
|
||
|
|
||
|
def expect_float_(self):
|
||
|
self.advance_lexer_()
|
||
|
if self.cur_token_type_ is Lexer.FLOAT:
|
||
|
return self.cur_token_
|
||
|
raise FeatureLibError(
|
||
|
"Expected a floating-point number", self.cur_token_location_
|
||
|
)
|
||
|
|
||
|
def expect_decipoint_(self):
|
||
|
if self.next_token_type_ == Lexer.FLOAT:
|
||
|
return self.expect_float_()
|
||
|
elif self.next_token_type_ is Lexer.NUMBER:
|
||
|
return self.expect_number_() / 10
|
||
|
else:
|
||
|
raise FeatureLibError(
|
||
|
"Expected an integer or floating-point number", self.cur_token_location_
|
||
|
)
|
||
|
|
||
|
def expect_stat_flags(self):
|
||
|
value = 0
|
||
|
flags = {
|
||
|
"OlderSiblingFontAttribute": 1,
|
||
|
"ElidableAxisValueName": 2,
|
||
|
}
|
||
|
while self.next_token_ != ";":
|
||
|
if self.next_token_ in flags:
|
||
|
name = self.expect_name_()
|
||
|
value = value | flags[name]
|
||
|
else:
|
||
|
raise FeatureLibError(
|
||
|
f"Unexpected STAT flag {self.cur_token_}", self.cur_token_location_
|
||
|
)
|
||
|
return value
|
||
|
|
||
|
def expect_stat_values_(self):
|
||
|
if self.next_token_type_ == Lexer.FLOAT:
|
||
|
return self.expect_float_()
|
||
|
elif self.next_token_type_ is Lexer.NUMBER:
|
||
|
return self.expect_number_()
|
||
|
else:
|
||
|
raise FeatureLibError(
|
||
|
"Expected an integer or floating-point number", self.cur_token_location_
|
||
|
)
|
||
|
|
||
|
def expect_string_(self):
|
||
|
self.advance_lexer_()
|
||
|
if self.cur_token_type_ is Lexer.STRING:
|
||
|
return self.cur_token_
|
||
|
raise FeatureLibError("Expected a string", self.cur_token_location_)
|
||
|
|
||
|
def advance_lexer_(self, comments=False):
|
||
|
if comments and self.cur_comments_:
|
||
|
self.cur_token_type_ = Lexer.COMMENT
|
||
|
self.cur_token_, self.cur_token_location_ = self.cur_comments_.pop(0)
|
||
|
return
|
||
|
else:
|
||
|
self.cur_token_type_, self.cur_token_, self.cur_token_location_ = (
|
||
|
self.next_token_type_,
|
||
|
self.next_token_,
|
||
|
self.next_token_location_,
|
||
|
)
|
||
|
while True:
|
||
|
try:
|
||
|
(
|
||
|
self.next_token_type_,
|
||
|
self.next_token_,
|
||
|
self.next_token_location_,
|
||
|
) = next(self.lexer_)
|
||
|
except StopIteration:
|
||
|
self.next_token_type_, self.next_token_ = (None, None)
|
||
|
if self.next_token_type_ != Lexer.COMMENT:
|
||
|
break
|
||
|
self.cur_comments_.append((self.next_token_, self.next_token_location_))
|
||
|
|
||
|
@staticmethod
|
||
|
def reverse_string_(s):
|
||
|
"""'abc' --> 'cba'"""
|
||
|
return "".join(reversed(list(s)))
|
||
|
|
||
|
def make_cid_range_(self, location, start, limit):
|
||
|
"""(location, 999, 1001) --> ["cid00999", "cid01000", "cid01001"]"""
|
||
|
result = list()
|
||
|
if start > limit:
|
||
|
raise FeatureLibError(
|
||
|
"Bad range: start should be less than limit", location
|
||
|
)
|
||
|
for cid in range(start, limit + 1):
|
||
|
result.append("cid%05d" % cid)
|
||
|
return result
|
||
|
|
||
|
def make_glyph_range_(self, location, start, limit):
|
||
|
"""(location, "a.sc", "d.sc") --> ["a.sc", "b.sc", "c.sc", "d.sc"]"""
|
||
|
result = list()
|
||
|
if len(start) != len(limit):
|
||
|
raise FeatureLibError(
|
||
|
'Bad range: "%s" and "%s" should have the same length' % (start, limit),
|
||
|
location,
|
||
|
)
|
||
|
|
||
|
rev = self.reverse_string_
|
||
|
prefix = os.path.commonprefix([start, limit])
|
||
|
suffix = rev(os.path.commonprefix([rev(start), rev(limit)]))
|
||
|
if len(suffix) > 0:
|
||
|
start_range = start[len(prefix) : -len(suffix)]
|
||
|
limit_range = limit[len(prefix) : -len(suffix)]
|
||
|
else:
|
||
|
start_range = start[len(prefix) :]
|
||
|
limit_range = limit[len(prefix) :]
|
||
|
|
||
|
if start_range >= limit_range:
|
||
|
raise FeatureLibError(
|
||
|
"Start of range must be smaller than its end", location
|
||
|
)
|
||
|
|
||
|
uppercase = re.compile(r"^[A-Z]$")
|
||
|
if uppercase.match(start_range) and uppercase.match(limit_range):
|
||
|
for c in range(ord(start_range), ord(limit_range) + 1):
|
||
|
result.append("%s%c%s" % (prefix, c, suffix))
|
||
|
return result
|
||
|
|
||
|
lowercase = re.compile(r"^[a-z]$")
|
||
|
if lowercase.match(start_range) and lowercase.match(limit_range):
|
||
|
for c in range(ord(start_range), ord(limit_range) + 1):
|
||
|
result.append("%s%c%s" % (prefix, c, suffix))
|
||
|
return result
|
||
|
|
||
|
digits = re.compile(r"^[0-9]{1,3}$")
|
||
|
if digits.match(start_range) and digits.match(limit_range):
|
||
|
for i in range(int(start_range, 10), int(limit_range, 10) + 1):
|
||
|
number = ("000" + str(i))[-len(start_range) :]
|
||
|
result.append("%s%s%s" % (prefix, number, suffix))
|
||
|
return result
|
||
|
|
||
|
raise FeatureLibError('Bad range: "%s-%s"' % (start, limit), location)
|
||
|
|
||
|
|
||
|
class SymbolTable(object):
|
||
|
def __init__(self):
|
||
|
self.scopes_ = [{}]
|
||
|
|
||
|
def enter_scope(self):
|
||
|
self.scopes_.append({})
|
||
|
|
||
|
def exit_scope(self):
|
||
|
self.scopes_.pop()
|
||
|
|
||
|
def define(self, name, item):
|
||
|
self.scopes_[-1][name] = item
|
||
|
|
||
|
def resolve(self, name):
|
||
|
for scope in reversed(self.scopes_):
|
||
|
item = scope.get(name)
|
||
|
if item:
|
||
|
return item
|
||
|
return None
|