2015-08-21 17:09:46 +02:00
from fontTools . feaLib . error import FeatureLibError
2018-01-22 18:27:36 +00:00
from fontTools . feaLib . lexer import Lexer , IncludingLexer , NonIncludingLexer
2017-02-14 12:39:41 +01:00
from fontTools . misc . encodingTools import getEncoding
2016-03-14 23:44:25 +04:00
from fontTools . misc . py23 import *
2015-08-01 14:49:19 +02:00
import fontTools . feaLib . ast as ast
2016-03-21 11:01:33 +00:00
import logging
2015-08-01 17:34:02 +02:00
import os
import re
2015-08-01 14:49:19 +02:00
2015-08-01 12:35:22 +02:00
2016-03-21 11:01:33 +00:00
log = logging . getLogger ( __name__ )
2015-08-01 12:35:22 +02:00
class Parser ( object ) :
2020-05-12 23:11:17 +01:00
""" 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
2020-05-28 17:43:19 +01:00
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 .
2020-05-12 23:11:17 +01:00
"""
2020-07-15 17:16:12 +01:00
2017-01-04 16:18:50 +00:00
extensions = { }
ast = ast
2020-07-15 17:16:12 +01:00
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 ) }
2017-01-12 13:36:06 +00:00
2020-07-15 17:16:12 +01:00
def __init__ (
self , featurefile , glyphNames = ( ) , followIncludes = True , includeDir = None , * * kwargs
) :
2020-05-12 23:11:17 +01:00
2017-11-16 14:25:43 +00:00
if " glyphMap " in kwargs :
from fontTools . misc . loggingTools import deprecateArgument
2020-07-15 17:16:12 +01:00
2017-11-16 14:25:43 +00:00
deprecateArgument ( " glyphMap " , " use ' glyphNames ' (iterable) instead " )
if glyphNames :
2020-07-15 17:16:12 +01:00
raise TypeError (
" ' glyphNames ' and (deprecated) ' glyphMap ' are " " mutually exclusive "
)
2017-11-16 14:25:43 +00:00
glyphNames = kwargs . pop ( " glyphMap " )
if kwargs :
2020-07-15 17:16:12 +01:00
raise TypeError (
" unsupported keyword argument %s : %s "
% ( " " if len ( kwargs ) == 1 else " s " , " , " . join ( repr ( k ) for k in kwargs ) )
)
2017-11-16 14:25:43 +00:00
2017-11-16 13:46:27 +00:00
self . glyphNames_ = set ( glyphNames )
2017-01-04 16:18:50 +00:00
self . doc_ = self . ast . FeatureFile ( )
2015-08-11 12:53:30 +02:00
self . anchors_ = SymbolTable ( )
2015-08-03 09:37:27 +02:00
self . glyphclasses_ = SymbolTable ( )
2015-08-11 10:59:26 +02:00
self . lookups_ = SymbolTable ( )
2015-08-03 09:37:27 +02:00
self . valuerecords_ = SymbolTable ( )
2020-07-15 17:16:12 +01:00
self . symbol_tables_ = { self . anchors_ , self . valuerecords_ }
2015-08-01 12:35:22 +02:00
self . next_token_type_ , self . next_token_ = ( None , None )
2017-03-08 13:50:06 +00:00
self . cur_comments_ = [ ]
2015-08-01 12:35:22 +02:00
self . next_token_location_ = None
2018-01-22 18:27:36 +00:00
lexerClass = IncludingLexer if followIncludes else NonIncludingLexer
2020-05-28 17:43:19 +01:00
self . lexer_ = lexerClass ( featurefile , includeDir = includeDir )
2017-03-08 15:19:09 +00:00
self . advance_lexer_ ( comments = True )
2015-08-01 12:35:22 +02:00
def parse ( self ) :
2020-05-12 23:11:17 +01:00
""" 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 . """
2015-08-04 11:01:04 +02:00
statements = self . doc_ . statements
2018-01-22 17:07:28 +00:00
while self . next_token_type_ is not None or self . cur_comments_ :
2017-03-08 15:19:09 +00:00
self . advance_lexer_ ( comments = True )
2017-03-08 13:50:06 +00:00
if self . cur_token_type_ is Lexer . COMMENT :
2018-02-27 18:38:15 +00:00
statements . append (
2020-07-15 17:16:12 +01:00
self . ast . Comment ( self . cur_token_ , location = self . cur_token_location_ )
)
2018-01-22 18:27:36 +00:00
elif self . is_cur_keyword_ ( " include " ) :
statements . append ( self . parse_include_ ( ) )
2017-03-08 13:50:06 +00:00
elif self . cur_token_type_ is Lexer . GLYPHCLASS :
2015-08-04 11:01:04 +02:00
statements . append ( self . parse_glyphclass_definition_ ( ) )
2016-09-16 18:57:40 +02:00
elif self . is_cur_keyword_ ( ( " anon " , " anonymous " ) ) :
statements . append ( self . parse_anonymous_ ( ) )
2015-08-11 12:53:30 +02:00
elif self . is_cur_keyword_ ( " anchorDef " ) :
statements . append ( self . parse_anchordef_ ( ) )
2015-08-01 17:34:02 +02:00
elif self . is_cur_keyword_ ( " languagesystem " ) :
2015-08-11 12:55:09 +02:00
statements . append ( self . parse_languagesystem_ ( ) )
2015-08-11 10:59:26 +02:00
elif self . is_cur_keyword_ ( " lookup " ) :
statements . append ( self . parse_lookup_ ( vertical = False ) )
2015-12-08 17:04:21 +01:00
elif self . is_cur_keyword_ ( " markClass " ) :
2015-12-12 12:54:23 +01:00
statements . append ( self . parse_markClass_ ( ) )
2015-08-01 17:34:02 +02:00
elif self . is_cur_keyword_ ( " feature " ) :
2015-08-11 10:19:39 +02:00
statements . append ( self . parse_feature_block_ ( ) )
2016-01-07 16:39:35 +01:00
elif self . is_cur_keyword_ ( " table " ) :
statements . append ( self . parse_table_ ( ) )
2015-08-11 12:53:30 +02:00
elif self . is_cur_keyword_ ( " valueRecordDef " ) :
2020-07-15 17:16:12 +01:00
statements . append ( self . parse_valuerecord_definition_ ( vertical = False ) )
elif (
self . cur_token_type_ is Lexer . NAME
and self . cur_token_ in self . extensions
) :
2017-01-04 16:18:50 +00:00
statements . append ( self . extensions [ self . cur_token_ ] ( self ) )
2016-07-27 09:54:08 +01:00
elif self . cur_token_type_ is Lexer . SYMBOL and self . cur_token_ == " ; " :
continue
2015-08-01 17:34:02 +02:00
else :
2015-12-08 17:04:21 +01:00
raise FeatureLibError (
" Expected feature, languagesystem, lookup, markClass, "
2020-07-15 17:16:12 +01:00
' table, or glyph class definition, got {} " {} " ' . format (
self . cur_token_type_ , self . cur_token_
) ,
self . cur_token_location_ ,
)
2015-08-01 12:35:22 +02:00
return self . doc_
2015-12-07 22:48:10 +01:00
def parse_anchor_ ( self ) :
2020-05-12 23:11:17 +01:00
# Parses an anchor in any of the four formats given in the feature
# file specification (2.e.vii).
2015-12-07 22:48:10 +01:00
self . expect_symbol_ ( " < " )
self . expect_keyword_ ( " anchor " )
location = self . cur_token_location_
2020-07-15 17:16:12 +01:00
if self . next_token_ == " NULL " : # Format D
2015-12-07 22:48:10 +01:00
self . expect_keyword_ ( " NULL " )
self . expect_symbol_ ( " > " )
return None
2020-07-15 17:16:12 +01:00
if self . next_token_type_ == Lexer . NAME : # Format E
2015-12-07 22:48:10 +01:00
name = self . expect_name_ ( )
anchordef = self . anchors_ . resolve ( name )
if anchordef is None :
raise FeatureLibError (
2020-07-15 17:16:12 +01:00
' Unknown anchor " %s " ' % name , self . cur_token_location_
)
2015-12-07 22:48:10 +01:00
self . expect_symbol_ ( " > " )
2020-07-15 17:16:12 +01:00
return self . ast . Anchor (
anchordef . x ,
anchordef . y ,
name = name ,
contourpoint = anchordef . contourpoint ,
xDeviceTable = None ,
yDeviceTable = None ,
location = location ,
)
2015-12-07 22:48:10 +01:00
x , y = self . expect_number_ ( ) , self . expect_number_ ( )
contourpoint = None
2020-07-15 17:16:12 +01:00
if self . next_token_ == " contourpoint " : # Format B
2015-12-07 22:48:10 +01:00
self . expect_keyword_ ( " contourpoint " )
contourpoint = self . expect_number_ ( )
2020-07-15 17:16:12 +01:00
if self . next_token_ == " < " : # Format C
2015-12-07 22:48:10 +01:00
xDeviceTable = self . parse_device_ ( )
yDeviceTable = self . parse_device_ ( )
else :
xDeviceTable , yDeviceTable = None , None
self . expect_symbol_ ( " > " )
2020-07-15 17:16:12 +01:00
return self . ast . Anchor (
x ,
y ,
name = None ,
contourpoint = contourpoint ,
xDeviceTable = xDeviceTable ,
yDeviceTable = yDeviceTable ,
location = location ,
)
2015-12-07 22:48:10 +01:00
2015-12-08 23:46:04 +01:00
def parse_anchor_marks_ ( self ) :
2020-05-12 23:11:17 +01:00
# Parses a sequence of ``[<anchor> mark @MARKCLASS]*.``
2017-01-04 16:18:50 +00:00
anchorMarks = [ ] # [(self.ast.Anchor, markClassName)*]
2015-12-08 23:46:04 +01:00
while self . next_token_ == " < " :
anchor = self . parse_anchor_ ( )
2015-12-09 10:40:49 +01:00
if anchor is None and self . next_token_ != " mark " :
continue # <anchor NULL> without mark, eg. in GPOS type 5
2015-12-08 23:46:04 +01:00
self . expect_keyword_ ( " mark " )
markClass = self . expect_markClass_reference_ ( )
anchorMarks . append ( ( anchor , markClass ) )
return anchorMarks
2015-08-11 12:53:30 +02:00
def parse_anchordef_ ( self ) :
2020-05-12 23:11:17 +01:00
# Parses a named anchor definition (`section 2.e.viii <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#2.e.vii>`_).
2015-08-11 12:53:30 +02:00
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_ ( " ; " )
2020-07-15 17:16:12 +01:00
anchordef = self . ast . AnchorDefinition (
name , x , y , contourpoint = contourpoint , location = location
)
2015-08-11 12:53:30 +02:00
self . anchors_ . define ( name , anchordef )
return anchordef
2016-09-16 18:57:40 +02:00
def parse_anonymous_ ( self ) :
2020-05-12 23:11:17 +01:00
# Parses an anonymous data block (`section 10 <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#10>`_).
2016-09-16 18:57:40 +02:00
assert self . is_cur_keyword_ ( ( " anon " , " anonymous " ) )
tag = self . expect_tag_ ( )
_ , content , location = self . lexer_ . scan_anonymous_block ( tag )
self . advance_lexer_ ( )
2020-07-15 17:16:12 +01:00
self . expect_symbol_ ( " } " )
2016-09-16 18:57:40 +02:00
end_tag = self . expect_tag_ ( )
assert tag == end_tag , " bad splitting in Lexer.scan_anonymous_block() "
2020-07-15 17:16:12 +01:00
self . expect_symbol_ ( " ; " )
2018-02-27 18:38:15 +00:00
return self . ast . AnonymousBlock ( tag , content , location = location )
2016-09-16 18:57:40 +02:00
2016-01-08 08:32:47 +01:00
def parse_attach_ ( self ) :
2020-05-12 23:11:17 +01:00
# Parses a GDEF Attach statement (`section 9.b <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.b>`_)
2016-01-08 08:32:47 +01:00
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_ ( " ; " )
2020-07-15 17:16:12 +01:00
return self . ast . AttachStatement ( glyphs , contourPoints , location = location )
2016-01-08 08:32:47 +01:00
2015-12-07 17:18:18 +01:00
def parse_enumerate_ ( self , vertical ) :
2020-05-12 23:11:17 +01:00
# Parse an enumerated pair positioning rule (`section 6.b.ii <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#6.b.ii>`_).
2015-12-07 17:18:18 +01:00
assert self . cur_token_ in { " enumerate " , " enum " }
self . advance_lexer_ ( )
return self . parse_position_ ( enumerated = True , vertical = vertical )
2016-01-08 17:11:29 +01:00
def parse_GlyphClassDef_ ( self ) :
2020-05-12 23:11:17 +01:00
# Parses 'GlyphClassDef @BASE, @LIGATURES, @MARKS, @COMPONENTS;'
2016-01-08 17:11:29 +01:00
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_ != " , " :
2016-01-08 19:06:52 +01:00
ligatureGlyphs = self . parse_glyphclass_ ( accept_glyphname = False )
2016-01-08 17:11:29 +01:00
else :
2016-01-08 19:06:52 +01:00
ligatureGlyphs = None
2016-01-08 17:11:29 +01:00
self . expect_symbol_ ( " , " )
if self . next_token_ != " , " :
2016-01-08 19:06:52 +01:00
markGlyphs = self . parse_glyphclass_ ( accept_glyphname = False )
2016-01-08 17:11:29 +01:00
else :
2016-01-08 19:06:52 +01:00
markGlyphs = None
2016-01-08 17:11:29 +01:00
self . expect_symbol_ ( " , " )
if self . next_token_ != " ; " :
componentGlyphs = self . parse_glyphclass_ ( accept_glyphname = False )
else :
componentGlyphs = None
self . expect_symbol_ ( " ; " )
2020-07-15 17:16:12 +01:00
return self . ast . GlyphClassDefStatement (
baseGlyphs , markGlyphs , ligatureGlyphs , componentGlyphs , location = location
)
2016-01-08 17:11:29 +01:00
2015-08-01 17:34:02 +02:00
def parse_glyphclass_definition_ ( self ) :
2020-05-12 23:11:17 +01:00
# Parses glyph class definitions such as '@UPPERCASE = [A-Z];'
2015-08-01 17:34:02 +02:00
location , name = self . cur_token_location_ , self . cur_token_
self . expect_symbol_ ( " = " )
2016-12-20 11:06:32 +00:00
glyphs = self . parse_glyphclass_ ( accept_glyphname = False )
2015-08-01 17:34:02 +02:00
self . expect_symbol_ ( " ; " )
2020-07-15 17:16:12 +01:00
glyphclass = self . ast . GlyphClassDefinition ( name , glyphs , location = location )
2015-08-03 09:37:27 +02:00
self . glyphclasses_ . define ( name , glyphclass )
2015-08-01 19:58:54 +02:00
return glyphclass
2015-08-01 17:34:02 +02:00
2017-02-11 12:05:01 +01:00
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 : ] )
2017-11-16 13:46:27 +00:00
if start in self . glyphNames_ and limit in self . glyphNames_ :
2017-02-11 12:05:01 +01:00
solutions . append ( ( start , limit ) )
if len ( solutions ) == 1 :
start , limit = solutions [ 0 ]
return start , limit
elif len ( solutions ) == 0 :
raise FeatureLibError (
2020-07-15 17:16:12 +01:00
' " %s " is not a glyph in the font, and it can not be split '
" into a range of known glyphs " % name ,
location ,
)
2017-02-11 12:05:01 +01:00
else :
2020-07-15 17:16:12 +01:00
ranges = " or " . join ( [ ' " %s - %s " ' % ( s , l ) for s , l in solutions ] )
2017-02-11 12:05:01 +01:00
raise FeatureLibError (
2020-07-15 17:16:12 +01:00
' Ambiguous glyph range " %s " ; '
2017-02-11 12:05:01 +01:00
" please use %s to clarify what you mean " % ( name , ranges ) ,
2020-07-15 17:16:12 +01:00
location ,
)
2017-02-11 12:05:01 +01:00
2021-02-03 14:12:46 +00:00
def parse_glyphclass_ ( self , accept_glyphname , accept_null = False ) :
2020-05-12 23:11:17 +01:00
# Parses a glyph class, either named or anonymous, or (if
2021-02-03 14:12:46 +00:00
# ``bool(accept_glyphname)``) a glyph name. If ``bool(accept_null)`` then
# also accept the special NULL glyph.
2020-07-15 17:16:12 +01:00
if accept_glyphname and self . next_token_type_ in ( Lexer . NAME , Lexer . CID ) :
2021-02-03 14:12:46 +00:00
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_ )
2016-01-12 10:58:00 +01:00
glyph = self . expect_glyph_ ( )
2020-02-13 14:47:29 +00:00
self . check_glyph_name_in_glyph_set ( glyph )
2018-02-27 18:38:15 +00:00
return self . ast . GlyphName ( glyph , location = self . cur_token_location_ )
2015-08-01 19:25:48 +02:00
if self . next_token_type_ is Lexer . GLYPHCLASS :
self . advance_lexer_ ( )
2015-08-03 09:37:27 +02:00
gc = self . glyphclasses_ . resolve ( self . cur_token_ )
2015-08-01 19:25:48 +02:00
if gc is None :
2015-08-21 17:09:46 +02:00
raise FeatureLibError (
" Unknown glyph class @ %s " % self . cur_token_ ,
2020-07-15 17:16:12 +01:00
self . cur_token_location_ ,
)
2017-01-04 16:18:50 +00:00
if isinstance ( gc , self . ast . MarkClass ) :
2020-07-15 17:16:12 +01:00
return self . ast . MarkClassName ( gc , location = self . cur_token_location_ )
2016-01-07 13:02:21 +01:00
else :
2020-07-15 17:16:12 +01:00
return self . ast . GlyphClassName ( gc , location = self . cur_token_location_ )
2015-08-01 19:25:48 +02:00
2015-08-01 17:34:02 +02:00
self . expect_symbol_ ( " [ " )
2015-12-11 15:16:22 +01:00
location = self . cur_token_location_
2018-02-27 18:38:15 +00:00
glyphs = self . ast . GlyphClass ( location = location )
2015-08-01 17:34:02 +02:00
while self . next_token_ != " ] " :
2016-01-13 16:31:05 +01:00
if self . next_token_type_ is Lexer . NAME :
glyph = self . expect_glyph_ ( )
2017-02-11 12:05:01 +01:00
location = self . cur_token_location_
2020-07-15 17:16:12 +01:00
if " - " in glyph and self . glyphNames_ and glyph not in self . glyphNames_ :
2017-02-11 12:05:01 +01:00
start , limit = self . split_glyph_range_ ( glyph , location )
2020-02-13 14:47:29 +00:00
self . check_glyph_name_in_glyph_set ( start , limit )
2017-02-11 12:05:01 +01:00
glyphs . add_range (
2020-07-15 17:16:12 +01:00
start , limit , self . make_glyph_range_ ( location , start , limit )
)
2017-02-11 12:05:01 +01:00
elif self . next_token_ == " - " :
start = glyph
2015-08-01 17:34:02 +02:00
self . expect_symbol_ ( " - " )
2017-02-11 12:05:01 +01:00
limit = self . expect_glyph_ ( )
2020-02-13 14:47:29 +00:00
self . check_glyph_name_in_glyph_set ( start , limit )
2017-02-11 12:05:01 +01:00
glyphs . add_range (
2020-07-15 17:16:12 +01:00
start , limit , self . make_glyph_range_ ( location , start , limit )
)
2015-08-01 17:34:02 +02:00
else :
2020-07-15 17:16:12 +01:00
if " - " in glyph and not self . glyphNames_ :
log . warning (
str (
FeatureLibError (
f " Ambiguous glyph name that looks like a range: { glyph !r} " ,
location ,
)
)
)
2020-02-13 14:47:29 +00:00
self . check_glyph_name_in_glyph_set ( glyph )
2016-04-21 10:46:52 +01:00
glyphs . append ( glyph )
2016-01-13 16:31:05 +01:00
elif self . next_token_type_ is Lexer . CID :
glyph = self . expect_glyph_ ( )
2016-01-12 10:58:00 +01:00
if self . next_token_ == " - " :
range_location = self . cur_token_location_
range_start = self . cur_token_
self . expect_symbol_ ( " - " )
range_end = self . expect_cid_ ( )
2020-02-13 14:47:29 +00:00
self . check_glyph_name_in_glyph_set (
2021-02-03 14:12:46 +00:00
f " cid { range_start : 05d } " ,
f " cid { range_end : 05d } " ,
2020-07-15 17:16:12 +01:00
)
glyphs . add_cid_range (
range_start ,
range_end ,
self . make_cid_range_ ( range_location , range_start , range_end ) ,
2020-02-13 14:47:29 +00:00
)
2016-01-12 10:58:00 +01:00
else :
2020-02-13 14:47:29 +00:00
glyph_name = f " cid { self . cur_token_ : 05d } "
self . check_glyph_name_in_glyph_set ( glyph_name )
glyphs . append ( glyph_name )
2016-01-13 16:31:05 +01:00
elif self . next_token_type_ is Lexer . GLYPHCLASS :
self . advance_lexer_ ( )
2015-08-03 09:37:27 +02:00
gc = self . glyphclasses_ . resolve ( self . cur_token_ )
2015-08-01 19:25:48 +02:00
if gc is None :
2015-08-21 17:09:46 +02:00
raise FeatureLibError (
2015-08-01 19:25:48 +02:00
" Unknown glyph class @ %s " % self . cur_token_ ,
2020-07-15 17:16:12 +01:00
self . cur_token_location_ ,
)
2017-10-27 21:20:30 +02:00
if isinstance ( gc , self . ast . MarkClass ) :
2020-07-15 17:16:12 +01:00
gc = self . ast . MarkClassName ( gc , location = self . cur_token_location_ )
2017-10-27 21:20:30 +02:00
else :
2020-07-15 17:16:12 +01:00
gc = self . ast . GlyphClassName ( gc , location = self . cur_token_location_ )
2016-12-20 11:06:32 +00:00
glyphs . add_class ( gc )
2015-08-01 19:25:48 +02:00
else :
2015-08-21 17:09:46 +02:00
raise FeatureLibError (
2015-08-03 09:47:06 +02:00
" Expected glyph name, glyph range, "
2020-04-03 12:38:35 +01:00
f " or glyph class reference, found { self . next_token_ !r} " ,
2020-07-15 17:16:12 +01:00
self . next_token_location_ ,
)
2015-08-01 17:34:02 +02:00
self . expect_symbol_ ( " ] " )
2016-12-20 11:06:32 +00:00
return glyphs
2015-08-01 17:34:02 +02:00
2016-01-25 12:13:40 +01:00
def parse_glyph_pattern_ ( self , vertical ) :
2020-05-12 23:11:17 +01:00
# Parses a glyph pattern, including lookups and context, e.g.::
#
# a b
# a b c' d e
# a b c' lookup ChangeC d e
2016-01-25 12:13:40 +01:00
prefix , glyphs , lookups , values , suffix = ( [ ] , [ ] , [ ] , [ ] , [ ] )
2016-02-04 15:31:29 +01:00
hasMarks = False
2016-02-05 11:02:29 +01:00
while self . next_token_ not in { " by " , " from " , " ; " , " , " } :
2015-12-14 12:20:57 +01:00
gc = self . parse_glyphclass_ ( accept_glyphname = True )
2015-08-04 19:55:55 +02:00
marked = False
if self . next_token_ == " ' " :
self . expect_symbol_ ( " ' " )
2016-02-04 15:31:29 +01:00
hasMarks = marked = True
2015-08-04 19:55:55 +02:00
if marked :
2017-11-21 11:42:28 +01:00
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 " ,
2020-07-15 17:16:12 +01:00
self . cur_token_location_ ,
)
2015-08-05 10:41:04 +02:00
glyphs . append ( gc )
elif glyphs :
suffix . append ( gc )
2015-08-04 19:55:55 +02:00
else :
2015-08-05 10:41:04 +02:00
prefix . append ( gc )
2015-08-11 12:22:07 +02:00
2016-01-25 12:13:40 +01:00
if self . is_next_value_ ( ) :
values . append ( self . parse_valuerecord_ ( vertical ) )
else :
values . append ( None )
2020-05-12 06:28:25 +01:00
lookuplist = None
while self . next_token_ == " lookup " :
if lookuplist is None :
lookuplist = [ ]
2015-08-11 12:22:07 +02:00
self . expect_keyword_ ( " lookup " )
if not marked :
2015-08-21 17:09:46 +02:00
raise FeatureLibError (
" Lookups can only follow marked glyphs " ,
2020-07-15 17:16:12 +01:00
self . cur_token_location_ ,
)
2015-08-11 12:22:07 +02:00
lookup_name = self . expect_name_ ( )
lookup = self . lookups_ . resolve ( lookup_name )
if lookup is None :
2015-08-21 17:09:46 +02:00
raise FeatureLibError (
2020-07-15 17:16:12 +01:00
' Unknown lookup " %s " ' % lookup_name , self . cur_token_location_
)
2020-05-12 06:28:25 +01:00
lookuplist . append ( lookup )
2015-08-11 12:22:07 +02:00
if marked :
2020-05-12 06:28:25 +01:00
lookups . append ( lookuplist )
2015-08-11 12:22:07 +02:00
2015-08-05 10:41:04 +02:00
if not glyphs and not suffix : # eg., "sub f f i by"
2015-08-11 12:22:07 +02:00
assert lookups == [ ]
2016-02-04 15:31:29 +01:00
return ( [ ] , prefix , [ None ] * len ( prefix ) , values , [ ] , hasMarks )
2015-08-05 10:41:04 +02:00
else :
2020-07-15 17:16:12 +01:00
assert not any ( values [ : len ( prefix ) ] ) , values
format1 = values [ len ( prefix ) : ] [ : len ( glyphs ) ]
format2 = values [ ( len ( prefix ) + len ( glyphs ) ) : ] [ : len ( suffix ) ]
values = (
format2
if format2 and isinstance ( format2 [ 0 ] , self . ast . ValueRecord )
else format1
)
2016-02-04 15:31:29 +01:00
return ( prefix , glyphs , lookups , values , suffix , hasMarks )
2015-08-05 10:41:04 +02:00
2016-02-06 11:13:58 +01:00
def parse_chain_context_ ( self ) :
location = self . cur_token_location_
2020-07-15 17:16:12 +01:00
prefix , glyphs , lookups , values , suffix , hasMarks = self . parse_glyph_pattern_ (
vertical = False
)
2016-02-06 11:13:58 +01:00
chainContext = [ ( prefix , glyphs , suffix ) ]
hasLookups = any ( lookups )
while self . next_token_ == " , " :
self . expect_symbol_ ( " , " )
2020-07-15 17:16:12 +01:00
(
prefix ,
glyphs ,
lookups ,
values ,
suffix ,
hasMarks ,
) = self . parse_glyph_pattern_ ( vertical = False )
2016-02-06 11:13:58 +01:00
chainContext . append ( ( prefix , glyphs , suffix ) )
hasLookups = hasLookups or any ( lookups )
self . expect_symbol_ ( " ; " )
return chainContext , hasLookups
2015-08-05 10:41:04 +02:00
def parse_ignore_ ( self ) :
2020-05-12 23:11:17 +01:00
# Parses an ignore sub/pos rule.
2015-08-05 10:41:04 +02:00
assert self . is_cur_keyword_ ( " ignore " )
location = self . cur_token_location_
self . advance_lexer_ ( )
if self . cur_token_ in [ " substitute " , " sub " ] :
2016-02-06 11:13:58 +01:00
chainContext , hasLookups = self . parse_chain_context_ ( )
2016-02-05 11:02:29 +01:00
if hasLookups :
2016-01-25 12:13:40 +01:00
raise FeatureLibError (
2020-07-15 17:16:12 +01:00
' No lookups can be specified for " ignore sub " ' , location
)
return self . ast . IgnoreSubstStatement ( chainContext , location = location )
2016-02-06 11:13:58 +01:00
if self . cur_token_ in [ " position " , " pos " ] :
chainContext , hasLookups = self . parse_chain_context_ ( )
if hasLookups :
raise FeatureLibError (
2020-07-15 17:16:12 +01:00
' No lookups can be specified for " ignore pos " ' , location
)
return self . ast . IgnorePosStatement ( chainContext , location = location )
2015-08-21 17:09:46 +02:00
raise FeatureLibError (
2020-07-15 17:16:12 +01:00
' Expected " substitute " or " position " ' , self . cur_token_location_
)
2015-08-05 10:41:04 +02:00
2018-01-22 18:27:36 +00:00
def parse_include_ ( self ) :
assert self . cur_token_ == " include "
location = self . cur_token_location_
filename = self . expect_filename_ ( )
# self.expect_symbol_(";")
2018-02-27 18:38:15 +00:00
return ast . IncludeStatement ( filename , location = location )
2018-01-22 18:27:36 +00:00
2015-08-10 16:30:10 +02:00
def parse_language_ ( self ) :
assert self . is_cur_keyword_ ( " language " )
2015-09-08 09:26:24 +02:00
location = self . cur_token_location_
language = self . expect_language_tag_ ( )
2015-08-10 16:30:10 +02:00
include_default , required = ( True , False )
if self . next_token_ in { " exclude_dflt " , " include_dflt " } :
2020-07-15 17:16:12 +01:00
include_default = self . expect_name_ ( ) == " include_dflt "
2015-08-10 16:30:10 +02:00
if self . next_token_ == " required " :
self . expect_keyword_ ( " required " )
required = True
self . expect_symbol_ ( " ; " )
2020-07-15 17:16:12 +01:00
return self . ast . LanguageStatement (
language , include_default , required , location = location
)
2015-08-10 16:30:10 +02:00
2016-01-07 17:22:31 +01:00
def parse_ligatureCaretByIndex_ ( self ) :
assert self . is_cur_keyword_ ( " LigatureCaretByIndex " )
location = self . cur_token_location_
glyphs = self . parse_glyphclass_ ( accept_glyphname = True )
2016-12-20 10:10:29 +00:00
carets = [ self . expect_number_ ( ) ]
2016-01-07 17:22:31 +01:00
while self . next_token_ != " ; " :
2016-12-20 10:10:29 +00:00
carets . append ( self . expect_number_ ( ) )
2016-01-07 17:22:31 +01:00
self . expect_symbol_ ( " ; " )
2020-07-15 17:16:12 +01:00
return self . ast . LigatureCaretByIndexStatement ( glyphs , carets , location = location )
2016-01-07 17:22:31 +01:00
2016-01-07 16:39:35 +01:00
def parse_ligatureCaretByPos_ ( self ) :
assert self . is_cur_keyword_ ( " LigatureCaretByPos " )
location = self . cur_token_location_
glyphs = self . parse_glyphclass_ ( accept_glyphname = True )
2016-12-20 10:10:29 +00:00
carets = [ self . expect_number_ ( ) ]
2016-01-07 16:39:35 +01:00
while self . next_token_ != " ; " :
2016-12-20 10:10:29 +00:00
carets . append ( self . expect_number_ ( ) )
2016-01-07 16:39:35 +01:00
self . expect_symbol_ ( " ; " )
2020-07-15 17:16:12 +01:00
return self . ast . LigatureCaretByPosStatement ( glyphs , carets , location = location )
2016-01-07 16:39:35 +01:00
2015-08-11 10:59:26 +02:00
def parse_lookup_ ( self , vertical ) :
2020-05-12 23:11:17 +01:00
# Parses a ``lookup`` - either a lookup block, or a lookup reference
# inside a feature.
2015-08-11 10:59:26 +02:00
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 :
2020-07-15 17:16:12 +01:00
raise FeatureLibError (
' Unknown lookup " %s " ' % name , self . cur_token_location_
)
2015-08-11 10:59:26 +02:00
self . expect_symbol_ ( " ; " )
2020-07-15 17:16:12 +01:00
return self . ast . LookupReferenceStatement ( lookup , location = location )
2015-08-11 10:59:26 +02:00
use_extension = False
if self . next_token_ == " useExtension " :
self . expect_keyword_ ( " useExtension " )
use_extension = True
2018-02-27 18:38:15 +00:00
block = self . ast . LookupBlock ( name , use_extension , location = location )
2015-08-11 10:59:26 +02:00
self . parse_block_ ( block , vertical )
self . lookups_ . define ( name , block )
return block
2015-12-10 13:04:16 +01:00
def parse_lookupflag_ ( self ) :
2020-05-12 23:11:17 +01:00
# Parses a ``lookupflag`` statement, either specified by number or
# in words.
2015-12-10 13:04:16 +01:00
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_ ( " ; " )
2018-02-27 18:38:15 +00:00
return self . ast . LookupFlagStatement ( value , location = location )
2015-12-10 13:04:16 +01:00
# format A: "lookupflag RightToLeft MarkAttachmentType @M;"
2019-03-15 14:03:40 +02:00
value_seen = False
2015-12-10 13:04:16 +01:00
value , markAttachment , markFilteringSet = 0 , None , None
flags = {
2020-07-15 17:16:12 +01:00
" RightToLeft " : 1 ,
" IgnoreBaseGlyphs " : 2 ,
" IgnoreLigatures " : 4 ,
" IgnoreMarks " : 8 ,
2015-12-10 13:04:16 +01:00
}
seen = set ( )
while self . next_token_ != " ; " :
if self . next_token_ in seen :
raise FeatureLibError (
" %s can be specified only once " % self . next_token_ ,
2020-07-15 17:16:12 +01:00
self . next_token_location_ ,
)
2015-12-10 13:04:16 +01:00
seen . add ( self . next_token_ )
if self . next_token_ == " MarkAttachmentType " :
self . expect_keyword_ ( " MarkAttachmentType " )
2020-08-07 17:57:23 +02:00
markAttachment = self . parse_glyphclass_ ( accept_glyphname = False )
2015-12-10 15:54:12 +01:00
elif self . next_token_ == " UseMarkFilteringSet " :
2015-12-10 13:04:16 +01:00
self . expect_keyword_ ( " UseMarkFilteringSet " )
2020-08-07 17:57:23 +02:00
markFilteringSet = self . parse_glyphclass_ ( accept_glyphname = False )
2015-12-10 13:04:16 +01:00
elif self . next_token_ in flags :
2019-03-15 14:03:40 +02:00
value_seen = True
2015-12-10 13:04:16 +01:00
value = value | flags [ self . expect_name_ ( ) ]
else :
raise FeatureLibError (
' " %s " is not a recognized lookupflag ' % self . next_token_ ,
2020-07-15 17:16:12 +01:00
self . next_token_location_ ,
)
2015-12-10 13:04:16 +01:00
self . expect_symbol_ ( " ; " )
2019-03-15 14:03:40 +02:00
if not any ( [ value_seen , markAttachment , markFilteringSet ] ) :
raise FeatureLibError (
2020-07-15 17:16:12 +01:00
" lookupflag must have a value " , self . next_token_location_
)
2019-03-15 14:03:40 +02:00
2020-07-15 17:16:12 +01:00
return self . ast . LookupFlagStatement (
value ,
markAttachment = markAttachment ,
markFilteringSet = markFilteringSet ,
location = location ,
)
2015-12-10 13:04:16 +01:00
2015-12-08 17:04:21 +01:00
def parse_markClass_ ( self ) :
assert self . is_cur_keyword_ ( " markClass " )
location = self . cur_token_location_
2015-12-12 12:54:23 +01:00
glyphs = self . parse_glyphclass_ ( accept_glyphname = True )
2015-12-08 17:04:21 +01:00
anchor = self . parse_anchor_ ( )
2015-12-08 19:04:42 +01:00
name = self . expect_class_name_ ( )
2015-12-08 17:04:21 +01:00
self . expect_symbol_ ( " ; " )
markClass = self . doc_ . markClasses . get ( name )
if markClass is None :
2017-01-04 16:18:50 +00:00
markClass = self . ast . MarkClass ( name )
2015-12-08 17:04:21 +01:00
self . doc_ . markClasses [ name ] = markClass
2015-12-12 12:54:23 +01:00
self . glyphclasses_ . define ( name , markClass )
2020-07-15 17:16:12 +01:00
mcdef = self . ast . MarkClassDefinition (
markClass , anchor , glyphs , location = location
)
2015-12-12 12:54:23 +01:00
markClass . addDefinition ( mcdef )
return mcdef
2015-12-08 17:04:21 +01:00
2015-12-07 17:18:18 +01:00
def parse_position_ ( self , enumerated , vertical ) :
2015-12-04 11:16:43 +01:00
assert self . cur_token_ in { " position " , " pos " }
2015-12-08 19:04:42 +01:00
if self . next_token_ == " cursive " : # GPOS type 3
2015-12-08 23:46:04 +01:00
return self . parse_position_cursive_ ( enumerated , vertical )
2020-07-15 17:16:12 +01:00
elif self . next_token_ == " base " : # GPOS type 4
2015-12-08 23:46:04 +01:00
return self . parse_position_base_ ( enumerated , vertical )
2020-07-15 17:16:12 +01:00
elif self . next_token_ == " ligature " : # GPOS type 5
2015-12-09 10:40:49 +01:00
return self . parse_position_ligature_ ( enumerated , vertical )
2020-07-15 17:16:12 +01:00
elif self . next_token_ == " mark " : # GPOS type 6
2015-12-09 17:14:13 +01:00
return self . parse_position_mark_ ( enumerated , vertical )
2015-12-08 19:04:42 +01:00
2015-12-08 23:46:04 +01:00
location = self . cur_token_location_
2020-07-15 17:16:12 +01:00
prefix , glyphs , lookups , values , suffix , hasMarks = self . parse_glyph_pattern_ (
vertical
)
2016-01-25 12:13:40 +01:00
self . expect_symbol_ ( " ; " )
if any ( lookups ) :
# GPOS type 8: Chaining contextual positioning; explicit lookups
if any ( values ) :
raise FeatureLibError (
2020-07-15 17:16:12 +01:00
' If " lookup " is present, no values must be specified ' , location
)
2017-01-04 16:18:50 +00:00
return self . ast . ChainContextPosStatement (
2020-07-15 17:16:12 +01:00
prefix , glyphs , suffix , lookups , location = location
)
2015-12-09 22:56:24 +01:00
2016-01-25 12:13:40 +01:00
# Pair positioning, format A: "pos V 10 A -10;"
# Pair positioning, format B: "pos V A -20;"
2016-02-09 08:58:18 +01:00
if not prefix and not suffix and len ( glyphs ) == 2 and not hasMarks :
2016-01-25 12:13:40 +01:00
if values [ 0 ] is None : # Format B: "pos V A -20;"
values . reverse ( )
2017-01-04 16:18:50 +00:00
return self . ast . PairPosStatement (
2020-07-15 17:16:12 +01:00
glyphs [ 0 ] ,
values [ 0 ] ,
glyphs [ 1 ] ,
values [ 1 ] ,
2018-02-27 19:24:04 +00:00
enumerated = enumerated ,
2020-07-15 17:16:12 +01:00
location = location ,
)
2015-12-09 22:56:24 +01:00
2016-01-25 12:13:40 +01:00
if enumerated :
raise FeatureLibError (
2020-07-15 17:16:12 +01:00
' " enumerate " is only allowed with pair positionings ' , location
)
return self . ast . SinglePosStatement (
list ( zip ( glyphs , values ) ) ,
prefix ,
suffix ,
forceChain = hasMarks ,
location = location ,
)
2015-12-04 11:16:43 +01:00
2015-12-08 23:46:04 +01:00
def parse_position_cursive_ ( self , enumerated , vertical ) :
location = self . cur_token_location_
self . expect_keyword_ ( " cursive " )
if enumerated :
raise FeatureLibError (
2020-07-15 17:16:12 +01:00
' " enumerate " is not allowed with ' " cursive attachment positioning " ,
location ,
)
2016-12-20 10:10:29 +00:00
glyphclass = self . parse_glyphclass_ ( accept_glyphname = True )
2015-12-08 23:46:04 +01:00
entryAnchor = self . parse_anchor_ ( )
exitAnchor = self . parse_anchor_ ( )
self . expect_symbol_ ( " ; " )
2017-01-04 16:18:50 +00:00
return self . ast . CursivePosStatement (
2020-07-15 17:16:12 +01:00
glyphclass , entryAnchor , exitAnchor , location = location
)
2015-12-08 23:46:04 +01:00
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 '
2020-07-15 17:16:12 +01:00
" mark-to-base attachment positioning " ,
location ,
)
2016-12-20 10:10:29 +00:00
base = self . parse_glyphclass_ ( accept_glyphname = True )
2015-12-08 23:46:04 +01:00
marks = self . parse_anchor_marks_ ( )
self . expect_symbol_ ( " ; " )
2018-02-27 18:38:15 +00:00
return self . ast . MarkBasePosStatement ( base , marks , location = location )
2015-12-08 23:46:04 +01:00
2015-12-09 10:40:49 +01:00
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 '
2020-07-15 17:16:12 +01:00
" mark-to-ligature attachment positioning " ,
location ,
)
2016-12-20 10:10:29 +00:00
ligatures = self . parse_glyphclass_ ( accept_glyphname = True )
2015-12-09 10:40:49 +01:00
marks = [ self . parse_anchor_marks_ ( ) ]
while self . next_token_ == " ligComponent " :
self . expect_keyword_ ( " ligComponent " )
marks . append ( self . parse_anchor_marks_ ( ) )
self . expect_symbol_ ( " ; " )
2018-02-27 18:38:15 +00:00
return self . ast . MarkLigPosStatement ( ligatures , marks , location = location )
2015-12-09 10:40:49 +01:00
2015-12-09 17:14:13 +01:00
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 '
2020-07-15 17:16:12 +01:00
" mark-to-mark attachment positioning " ,
location ,
)
2016-12-20 10:10:29 +00:00
baseMarks = self . parse_glyphclass_ ( accept_glyphname = True )
2015-12-09 17:14:13 +01:00
marks = self . parse_anchor_marks_ ( )
self . expect_symbol_ ( " ; " )
2020-07-15 17:16:12 +01:00
return self . ast . MarkMarkPosStatement ( baseMarks , marks , location = location )
2015-12-09 17:14:13 +01:00
2015-08-10 11:30:47 +02:00
def parse_script_ ( self ) :
assert self . is_cur_keyword_ ( " script " )
2015-09-08 09:26:24 +02:00
location , script = self . cur_token_location_ , self . expect_script_tag_ ( )
2015-08-10 11:30:47 +02:00
self . expect_symbol_ ( " ; " )
2018-02-27 18:38:15 +00:00
return self . ast . ScriptStatement ( script , location = location )
2015-08-10 11:30:47 +02:00
2015-08-05 10:41:04 +02:00
def parse_substitute_ ( self ) :
2015-12-03 13:05:42 +01:00
assert self . cur_token_ in { " substitute " , " sub " , " reversesub " , " rsub " }
2015-08-05 10:41:04 +02:00
location = self . cur_token_location_
2015-12-03 13:05:42 +01:00
reverse = self . cur_token_ in { " reversesub " , " rsub " }
2020-07-15 17:16:12 +01:00
(
old_prefix ,
old ,
lookups ,
values ,
old_suffix ,
hasMarks ,
) = self . parse_glyph_pattern_ ( vertical = False )
2016-01-25 12:13:40 +01:00
if any ( values ) :
raise FeatureLibError (
2020-07-15 17:16:12 +01:00
" Substitution statements cannot contain values " , location
)
2015-08-11 16:45:54 +02:00
new = [ ]
2015-08-11 12:22:07 +02:00
if self . next_token_ == " by " :
2015-08-11 15:54:54 +02:00
keyword = self . expect_keyword_ ( " by " )
2015-08-11 16:45:54 +02:00
while self . next_token_ != " ; " :
2021-02-03 14:12:46 +00:00
gc = self . parse_glyphclass_ ( accept_glyphname = True , accept_null = True )
2016-01-06 16:15:26 +01:00
new . append ( gc )
2015-08-11 15:54:54 +02:00
elif self . next_token_ == " from " :
keyword = self . expect_keyword_ ( " from " )
2016-01-06 16:15:26 +01:00
new = [ self . parse_glyphclass_ ( accept_glyphname = False ) ]
2015-08-11 12:22:07 +02:00
else :
2015-08-11 15:54:54 +02:00
keyword = None
2015-08-04 19:55:55 +02:00
self . expect_symbol_ ( " ; " )
2020-01-30 10:47:57 +00:00
if len ( new ) == 0 and not any ( lookups ) :
2015-08-21 17:09:46 +02:00
raise FeatureLibError (
2015-08-11 15:54:54 +02:00
' Expected " by " , " from " or explicit lookup references ' ,
2020-07-15 17:16:12 +01:00
self . cur_token_location_ ,
)
2015-08-11 15:54:54 +02:00
2015-09-08 10:33:07 +02:00
# GSUB lookup type 3: Alternate substitution.
# Format: "substitute a from [a.1 a.2 a.3];"
2015-08-11 15:54:54 +02:00
if keyword == " from " :
2015-12-03 13:05:42 +01:00
if reverse :
raise FeatureLibError (
2020-07-15 17:16:12 +01:00
' Reverse chaining substitutions do not support " from " ' , location
)
2015-12-14 12:20:57 +01:00
if len ( old ) != 1 or len ( old [ 0 ] . glyphSet ( ) ) != 1 :
2020-07-15 17:16:12 +01:00
raise FeatureLibError ( ' Expected a single glyph before " from " ' , location )
2015-08-11 15:54:54 +02:00
if len ( new ) != 1 :
2015-08-21 17:09:46 +02:00
raise FeatureLibError (
2020-07-15 17:16:12 +01:00
' Expected a single glyphclass after " from " ' , location
)
2017-01-04 16:18:50 +00:00
return self . ast . AlternateSubstStatement (
2020-07-15 17:16:12 +01:00
old_prefix , old [ 0 ] , old_suffix , new [ 0 ] , location = location
)
2015-08-11 15:54:54 +02:00
2015-09-07 16:10:13 +02:00
num_lookups = len ( [ l for l in lookups if l is not None ] )
2015-09-08 10:33:07 +02:00
2021-02-03 14:16:57 +00:00
is_deletion = False
2021-02-03 14:12:46 +00:00
if len ( new ) == 1 and len ( new [ 0 ] . glyphSet ( ) ) == 0 :
new = [ ] # Deletion
2021-02-03 14:16:57 +00:00
is_deletion = True
2021-02-03 14:12:46 +00:00
2015-09-08 10:33:07 +02:00
# 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];"
2020-07-15 17:16:12 +01:00
if not reverse and len ( old ) == 1 and len ( new ) == 1 and num_lookups == 0 :
2016-04-21 10:46:52 +01:00
glyphs = list ( old [ 0 ] . glyphSet ( ) )
replacements = list ( new [ 0 ] . glyphSet ( ) )
2015-09-08 10:33:07 +02:00
if len ( replacements ) == 1 :
replacements = replacements * len ( glyphs )
if len ( glyphs ) != len ( replacements ) :
raise FeatureLibError (
' Expected a glyph class with %d elements after " by " , '
2020-07-15 17:16:12 +01:00
" but found a glyph class with %d elements "
% ( len ( glyphs ) , len ( replacements ) ) ,
location ,
)
2017-01-04 16:18:50 +00:00
return self . ast . SingleSubstStatement (
2020-07-15 17:16:12 +01:00
old , new , old_prefix , old_suffix , forceChain = hasMarks , location = location
2016-04-21 10:46:52 +01:00
)
2015-09-08 10:33:07 +02:00
2015-09-08 12:05:44 +02:00
# GSUB lookup type 2: Multiple substitution.
# Format: "substitute f_f_i by f f i;"
2020-07-15 17:16:12 +01:00
if (
not reverse
and len ( old ) == 1
and len ( old [ 0 ] . glyphSet ( ) ) == 1
2021-02-03 14:12:46 +00:00
and (
( len ( new ) > 1 and max ( [ len ( n . glyphSet ( ) ) for n in new ] ) == 1 )
or len ( new ) == 0
)
2020-07-15 17:16:12 +01:00
and num_lookups == 0
) :
2017-01-04 16:18:50 +00:00
return self . ast . MultipleSubstStatement (
2020-07-15 17:16:12 +01:00
old_prefix ,
tuple ( old [ 0 ] . glyphSet ( ) ) [ 0 ] ,
old_suffix ,
2019-02-19 04:42:28 +02:00
tuple ( [ list ( n . glyphSet ( ) ) [ 0 ] for n in new ] ) ,
2020-07-15 17:16:12 +01:00
forceChain = hasMarks ,
location = location ,
)
2015-09-08 12:05:44 +02:00
2015-09-08 10:33:07 +02:00
# GSUB lookup type 4: Ligature substitution.
# Format: "substitute f f i by f_f_i;"
2020-07-15 17:16:12 +01:00
if (
not reverse
and len ( old ) > 1
and len ( new ) == 1
and len ( new [ 0 ] . glyphSet ( ) ) == 1
and num_lookups == 0
) :
2017-01-04 16:18:50 +00:00
return self . ast . LigatureSubstStatement (
2020-07-15 17:16:12 +01:00
old_prefix ,
old ,
old_suffix ,
list ( new [ 0 ] . glyphSet ( ) ) [ 0 ] ,
forceChain = hasMarks ,
location = location ,
)
2015-09-07 16:10:13 +02:00
2015-12-03 13:05:42 +01:00
# 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 " ,
2020-07-15 17:16:12 +01:00
location ,
)
2015-12-03 13:05:42 +01:00
if len ( new ) != 1 :
raise FeatureLibError (
2020-07-15 17:16:12 +01:00
" In reverse chaining single substitutions, "
2015-12-03 13:05:42 +01:00
' the replacement (after " by " ) must be a single glyph '
2020-07-15 17:16:12 +01:00
" or glyph class " ,
location ,
)
2015-12-03 13:05:42 +01:00
if num_lookups != 0 :
raise FeatureLibError (
2020-07-15 17:16:12 +01:00
" Reverse chaining substitutions cannot call named lookups " , location
)
2015-12-14 12:20:57 +01:00
glyphs = sorted ( list ( old [ 0 ] . glyphSet ( ) ) )
2016-01-06 16:15:26 +01:00
replacements = sorted ( list ( new [ 0 ] . glyphSet ( ) ) )
2015-12-03 13:05:42 +01:00
if len ( replacements ) == 1 :
replacements = replacements * len ( glyphs )
if len ( glyphs ) != len ( replacements ) :
raise FeatureLibError (
' Expected a glyph class with %d elements after " by " , '
2020-07-15 17:16:12 +01:00
" but found a glyph class with %d elements "
% ( len ( glyphs ) , len ( replacements ) ) ,
location ,
)
2017-01-04 16:18:50 +00:00
return self . ast . ReverseChainSingleSubstStatement (
2020-07-15 17:16:12 +01:00
old_prefix , old_suffix , old , new , location = location
)
2015-12-03 13:05:42 +01:00
2018-01-24 11:35:07 +00:00
if len ( old ) > 1 and len ( new ) > 1 :
raise FeatureLibError (
2020-07-15 17:16:12 +01:00
" Direct substitution of multiple glyphs by multiple glyphs "
" is not supported " ,
location ,
)
2018-01-24 11:35:07 +00:00
2020-04-16 16:10:03 +01:00
# If there are remaining glyphs to parse, this is an invalid GSUB statement
2021-02-03 14:16:57 +00:00
if len ( new ) != 0 or is_deletion :
2020-07-15 17:16:12 +01:00
raise FeatureLibError ( " Invalid substitution statement " , location )
2020-04-16 16:10:03 +01:00
2015-12-09 13:58:05 +01:00
# GSUB lookup type 6: Chaining contextual substitution.
2017-01-04 16:18:50 +00:00
rule = self . ast . ChainContextSubstStatement (
2020-07-15 17:16:12 +01:00
old_prefix , old , old_suffix , lookups , location = location
)
2015-08-04 19:55:55 +02:00
return rule
2015-08-11 15:14:47 +02:00
def parse_subtable_ ( self ) :
assert self . is_cur_keyword_ ( " subtable " )
location = self . cur_token_location_
self . expect_symbol_ ( " ; " )
2018-02-27 18:38:15 +00:00
return self . ast . SubtableStatement ( location = location )
2015-08-11 15:14:47 +02:00
2016-03-18 13:18:31 +04:00
def parse_size_parameters_ ( self ) :
2020-05-12 23:11:17 +01:00
# Parses a ``parameters`` statement used in ``size`` features. See
# `section 8.b <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.b>`_.
2016-03-18 13:18:31 +04:00
assert self . is_cur_keyword_ ( " parameters " )
location = self . cur_token_location_
DesignSize = self . expect_decipoint_ ( )
SubfamilyID = self . expect_number_ ( )
2021-03-24 08:04:14 -07:00
RangeStart = 0.
RangeEnd = 0.
2020-07-15 17:16:12 +01:00
if self . next_token_type_ in ( Lexer . NUMBER , Lexer . FLOAT ) or SubfamilyID != 0 :
2016-03-18 13:18:31 +04:00
RangeStart = self . expect_decipoint_ ( )
RangeEnd = self . expect_decipoint_ ( )
self . expect_symbol_ ( " ; " )
2020-07-15 17:16:12 +01:00
return self . ast . SizeParameters (
DesignSize , SubfamilyID , RangeStart , RangeEnd , location = location
)
2016-03-18 13:18:31 +04:00
def parse_size_menuname_ ( self ) :
assert self . is_cur_keyword_ ( " sizemenuname " )
location = self . cur_token_location_
platformID , platEncID , langID , string = self . parse_name_ ( )
2020-07-15 17:16:12 +01:00
return self . ast . FeatureNameStatement (
" size " , platformID , platEncID , langID , string , location = location
)
2016-03-18 13:18:31 +04:00
2016-01-07 16:39:35 +01:00
def parse_table_ ( self ) :
assert self . is_cur_keyword_ ( " table " )
location , name = self . cur_token_location_ , self . expect_tag_ ( )
2018-02-27 18:38:15 +00:00
table = self . ast . TableBlock ( name , location = location )
2016-01-07 16:39:35 +01:00
self . expect_symbol_ ( " { " )
handler = {
" GDEF " : self . parse_table_GDEF_ ,
2016-01-11 18:01:47 +01:00
" head " : self . parse_table_head_ ,
2016-04-09 17:42:38 +02:00
" hhea " : self . parse_table_hhea_ ,
2016-10-17 09:00:45 +01:00
" vhea " : self . parse_table_vhea_ ,
2016-03-14 23:44:25 +04:00
" name " : self . parse_table_name_ ,
2016-03-19 02:47:04 +04:00
" BASE " : self . parse_table_BASE_ ,
2016-03-19 22:05:38 +04:00
" OS/2 " : self . parse_table_OS_2_ ,
2020-08-11 21:21:30 -07:00
" STAT " : self . parse_table_STAT_ ,
2016-01-07 16:39:35 +01:00
} . get ( name )
if handler :
handler ( table )
else :
2020-07-15 17:16:12 +01:00
raise FeatureLibError (
' " table %s " is not supported ' % name . strip ( ) , location
)
2016-01-07 16:39:35 +01:00
self . expect_symbol_ ( " } " )
end_tag = self . expect_tag_ ( )
if end_tag != name :
2020-07-15 17:16:12 +01:00
raise FeatureLibError (
' Expected " %s " ' % name . strip ( ) , self . cur_token_location_
)
2016-01-07 16:39:35 +01:00
self . expect_symbol_ ( " ; " )
return table
def parse_table_GDEF_ ( self , table ) :
statements = table . statements
2017-03-09 14:38:51 +01:00
while self . next_token_ != " } " or self . cur_comments_ :
2017-03-08 15:19:09 +00:00
self . advance_lexer_ ( comments = True )
2017-03-08 13:50:06 +00:00
if self . cur_token_type_ is Lexer . COMMENT :
2020-07-15 17:16:12 +01:00
statements . append (
self . ast . Comment ( self . cur_token_ , location = self . cur_token_location_ )
)
2017-03-08 13:50:06 +00:00
elif self . is_cur_keyword_ ( " Attach " ) :
2016-01-08 08:32:47 +01:00
statements . append ( self . parse_attach_ ( ) )
2016-01-08 17:11:29 +01:00
elif self . is_cur_keyword_ ( " GlyphClassDef " ) :
statements . append ( self . parse_GlyphClassDef_ ( ) )
2016-01-08 08:32:47 +01:00
elif self . is_cur_keyword_ ( " LigatureCaretByIndex " ) :
2016-01-07 17:22:31 +01:00
statements . append ( self . parse_ligatureCaretByIndex_ ( ) )
elif self . is_cur_keyword_ ( " LigatureCaretByPos " ) :
2016-01-07 16:39:35 +01:00
statements . append ( self . parse_ligatureCaretByPos_ ( ) )
2017-03-09 13:56:47 +01:00
elif self . cur_token_ == " ; " :
continue
2016-01-07 16:39:35 +01:00
else :
raise FeatureLibError (
2020-07-15 17:16:12 +01:00
" Expected Attach, LigatureCaretByIndex, " " or LigatureCaretByPos " ,
self . cur_token_location_ ,
)
2016-01-07 16:39:35 +01:00
2016-01-11 18:01:47 +01:00
def parse_table_head_ ( self , table ) :
statements = table . statements
2017-03-09 14:38:51 +01:00
while self . next_token_ != " } " or self . cur_comments_ :
2017-03-08 15:19:09 +00:00
self . advance_lexer_ ( comments = True )
2017-03-08 13:50:06 +00:00
if self . cur_token_type_ is Lexer . COMMENT :
2020-07-15 17:16:12 +01:00
statements . append (
self . ast . Comment ( self . cur_token_ , location = self . cur_token_location_ )
)
2017-03-08 13:50:06 +00:00
elif self . is_cur_keyword_ ( " FontRevision " ) :
2016-01-11 18:01:47 +01:00
statements . append ( self . parse_FontRevision_ ( ) )
2017-03-09 13:56:47 +01:00
elif self . cur_token_ == " ; " :
continue
2016-01-11 18:01:47 +01:00
else :
2020-07-15 17:16:12 +01:00
raise FeatureLibError ( " Expected FontRevision " , self . cur_token_location_ )
2016-01-11 18:01:47 +01:00
2016-04-09 17:42:38 +02:00
def parse_table_hhea_ ( self , table ) :
statements = table . statements
fields = ( " CaretOffset " , " Ascender " , " Descender " , " LineGap " )
2017-03-09 14:38:51 +01:00
while self . next_token_ != " } " or self . cur_comments_ :
2017-03-08 15:19:09 +00:00
self . advance_lexer_ ( comments = True )
2017-03-08 13:50:06 +00:00
if self . cur_token_type_ is Lexer . COMMENT :
2020-07-15 17:16:12 +01:00
statements . append (
self . ast . Comment ( self . cur_token_ , location = self . cur_token_location_ )
)
2017-03-08 13:50:06 +00:00
elif self . cur_token_type_ is Lexer . NAME and self . cur_token_ in fields :
2016-04-09 17:42:38 +02:00
key = self . cur_token_ . lower ( )
value = self . expect_number_ ( )
statements . append (
2020-07-15 17:16:12 +01:00
self . ast . HheaField ( key , value , location = self . cur_token_location_ )
)
2017-03-08 13:50:06 +00:00
if self . next_token_ != " ; " :
2020-07-15 17:16:12 +01:00
raise FeatureLibError (
" Incomplete statement " , self . next_token_location_
)
2016-04-09 17:42:38 +02:00
elif self . cur_token_ == " ; " :
continue
else :
2020-07-15 17:16:12 +01:00
raise FeatureLibError (
" Expected CaretOffset, Ascender, " " Descender or LineGap " ,
self . cur_token_location_ ,
)
2016-04-09 17:42:38 +02:00
2016-10-17 09:00:45 +01:00
def parse_table_vhea_ ( self , table ) :
statements = table . statements
fields = ( " VertTypoAscender " , " VertTypoDescender " , " VertTypoLineGap " )
2017-03-09 14:38:51 +01:00
while self . next_token_ != " } " or self . cur_comments_ :
2017-03-08 15:19:09 +00:00
self . advance_lexer_ ( comments = True )
2017-03-08 13:50:06 +00:00
if self . cur_token_type_ is Lexer . COMMENT :
2020-07-15 17:16:12 +01:00
statements . append (
self . ast . Comment ( self . cur_token_ , location = self . cur_token_location_ )
)
2017-03-08 13:50:06 +00:00
elif self . cur_token_type_ is Lexer . NAME and self . cur_token_ in fields :
2016-10-17 09:00:45 +01:00
key = self . cur_token_ . lower ( )
value = self . expect_number_ ( )
statements . append (
2020-07-15 17:16:12 +01:00
self . ast . VheaField ( key , value , location = self . cur_token_location_ )
)
2017-03-08 13:50:06 +00:00
if self . next_token_ != " ; " :
2020-07-15 17:16:12 +01:00
raise FeatureLibError (
" Incomplete statement " , self . next_token_location_
)
2016-10-17 09:00:45 +01:00
elif self . cur_token_ == " ; " :
continue
else :
2020-07-15 17:16:12 +01:00
raise FeatureLibError (
" Expected VertTypoAscender, "
" VertTypoDescender or VertTypoLineGap " ,
self . cur_token_location_ ,
)
2016-10-17 09:00:45 +01:00
2016-03-14 23:44:25 +04:00
def parse_table_name_ ( self , table ) :
statements = table . statements
2017-03-09 14:38:51 +01:00
while self . next_token_ != " } " or self . cur_comments_ :
2017-03-08 15:19:09 +00:00
self . advance_lexer_ ( comments = True )
2017-03-08 13:50:06 +00:00
if self . cur_token_type_ is Lexer . COMMENT :
2020-07-15 17:16:12 +01:00
statements . append (
self . ast . Comment ( self . cur_token_ , location = self . cur_token_location_ )
)
2017-03-08 13:50:06 +00:00
elif self . is_cur_keyword_ ( " nameid " ) :
2016-03-14 23:44:25 +04:00
statement = self . parse_nameid_ ( )
if statement :
statements . append ( statement )
2017-03-09 13:56:47 +01:00
elif self . cur_token_ == " ; " :
continue
2016-03-14 23:44:25 +04:00
else :
2020-07-15 17:16:12 +01:00
raise FeatureLibError ( " Expected nameid " , self . cur_token_location_ )
2016-03-14 23:44:25 +04:00
2016-03-18 09:55:51 +04:00
def parse_name_ ( self ) :
2020-05-12 23:11:17 +01:00
""" Parses a name record. See `section 9.e <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.e>`_. """
2016-03-18 14:00:56 +04:00
platEncID = None
langID = None
2019-08-17 05:53:52 +02:00
if self . next_token_type_ in Lexer . NUMBERS :
platformID = self . expect_any_number_ ( )
2017-02-14 12:39:41 +01:00
location = self . cur_token_location_
2016-03-14 23:44:25 +04:00
if platformID not in ( 1 , 3 ) :
2017-02-14 12:39:41 +01:00
raise FeatureLibError ( " Expected platform id 1 or 3 " , location )
2019-08-17 05:53:52 +02:00
if self . next_token_type_ in Lexer . NUMBERS :
platEncID = self . expect_any_number_ ( )
langID = self . expect_any_number_ ( )
2016-03-14 23:44:25 +04:00
else :
platformID = 3
2017-02-14 12:39:41 +01:00
location = self . cur_token_location_
2016-03-14 23:44:25 +04:00
2020-07-15 17:16:12 +01:00
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
2016-03-14 23:44:25 +04:00
string = self . expect_string_ ( )
self . expect_symbol_ ( " ; " )
2017-02-14 12:39:41 +01:00
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
2016-03-18 09:55:51 +04:00
2020-08-11 21:21:30 -07:00
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
2016-03-18 09:55:51 +04:00
def parse_nameid_ ( self ) :
assert self . cur_token_ == " nameid " , self . cur_token_
2019-08-17 05:53:52 +02:00
location , nameID = self . cur_token_location_ , self . expect_any_number_ ( )
2017-07-15 16:44:17 -07:00
if nameID > 32767 :
2020-07-15 17:16:12 +01:00
raise FeatureLibError (
" Name id value cannot be greater than 32767 " , self . cur_token_location_
)
2016-03-18 09:55:51 +04:00
if 1 < = nameID < = 6 :
2020-07-15 17:16:12 +01:00
log . warning (
" Name id %d cannot be set from the feature file. "
" Ignoring record " % nameID
)
2016-03-21 11:01:33 +00:00
self . parse_name_ ( ) # skip to the next record
2016-03-18 09:55:51 +04:00
return None
platformID , platEncID , langID , string = self . parse_name_ ( )
2020-07-15 17:16:12 +01:00
return self . ast . NameRecord (
nameID , platformID , platEncID , langID , string , location = location
)
2016-03-14 23:44:25 +04:00
2017-02-14 12:39:41 +01:00
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 tounicode ( utf16 , " utf_16_be " )
@staticmethod
def unescape_unichr_ ( match ) :
n = match . group ( 0 ) [ 1 : ]
return unichr ( int ( n , 16 ) )
@staticmethod
def unescape_byte_ ( match , encoding ) :
n = match . group ( 0 ) [ 1 : ]
return bytechr ( int ( n , 16 ) ) . decode ( encoding )
2016-03-14 23:44:25 +04:00
2016-03-19 02:47:04 +04:00
def parse_table_BASE_ ( self , table ) :
statements = table . statements
2017-03-09 14:38:51 +01:00
while self . next_token_ != " } " or self . cur_comments_ :
2017-03-08 15:19:09 +00:00
self . advance_lexer_ ( comments = True )
2017-03-08 13:50:06 +00:00
if self . cur_token_type_ is Lexer . COMMENT :
2020-07-15 17:16:12 +01:00
statements . append (
self . ast . Comment ( self . cur_token_ , location = self . cur_token_location_ )
)
2017-03-08 13:50:06 +00:00
elif self . is_cur_keyword_ ( " HorizAxis.BaseTagList " ) :
2016-03-19 02:47:04 +04:00
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 (
2020-07-15 17:16:12 +01:00
self . ast . BaseAxis (
horiz_bases ,
horiz_scripts ,
False ,
location = self . cur_token_location_ ,
)
)
2016-03-19 02:47:04 +04:00
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 (
2020-07-15 17:16:12 +01:00
self . ast . BaseAxis (
vert_bases ,
vert_scripts ,
True ,
location = self . cur_token_location_ ,
)
)
2016-03-19 02:47:04 +04:00
elif self . cur_token_ == " ; " :
continue
2016-03-19 22:05:38 +04:00
def parse_table_OS_2_ ( self , table ) :
statements = table . statements
2020-07-15 17:16:12 +01:00
numbers = (
" FSType " ,
" TypoAscender " ,
" TypoDescender " ,
" TypoLineGap " ,
" winAscent " ,
" winDescent " ,
" XHeight " ,
" CapHeight " ,
" WeightClass " ,
" WidthClass " ,
" LowerOpSize " ,
" UpperOpSize " ,
)
2016-03-19 22:05:38 +04:00
ranges = ( " UnicodeRange " , " CodePageRange " )
2017-03-09 14:38:51 +01:00
while self . next_token_ != " } " or self . cur_comments_ :
2017-03-08 15:19:09 +00:00
self . advance_lexer_ ( comments = True )
2017-03-08 13:50:06 +00:00
if self . cur_token_type_ is Lexer . COMMENT :
2020-07-15 17:16:12 +01:00
statements . append (
self . ast . Comment ( self . cur_token_ , location = self . cur_token_location_ )
)
2017-03-08 13:50:06 +00:00
elif self . cur_token_type_ is Lexer . NAME :
2016-03-19 22:05:38 +04:00
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_ != " ; " :
2020-07-15 17:16:12 +01:00
value . append ( self . expect_number_ ( ) )
2016-03-19 22:05:38 +04:00
elif self . is_cur_keyword_ ( " Vendor " ) :
value = self . expect_string_ ( )
statements . append (
2020-07-15 17:16:12 +01:00
self . ast . OS2Field ( key , value , location = self . cur_token_location_ )
)
2016-03-19 22:05:38 +04:00
elif self . cur_token_ == " ; " :
continue
2020-08-11 21:21:30 -07:00
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 (
2021-02-25 16:59:47 +00:00
" stat " ,
platformID ,
platEncID ,
langID ,
string ,
location = self . cur_token_location_ ,
2020-08-11 21:21:30 -07:00
)
names . append ( nameRecord )
else :
if self . cur_token_ != " ; " :
2021-02-25 16:59:47 +00:00
raise FeatureLibError (
f " Unexpected token { self . cur_token_ } " f " in ElidedFallbackName " ,
self . cur_token_location_ ,
)
2020-08-11 21:21:30 -07:00
self . expect_symbol_ ( " } " )
if not names :
2021-02-25 16:59:47 +00:00
raise FeatureLibError ( ' Expected " name " ' , self . cur_token_location_ )
2020-08-11 21:21:30 -07:00
return names
def parse_STAT_design_axis ( self ) :
2021-02-25 16:59:47 +00:00
assert self . is_cur_keyword_ ( " DesignAxis " )
2020-08-11 21:21:30 -07:00
names = [ ]
axisTag = self . expect_tag_ ( )
2021-02-25 16:59:47 +00:00
if (
axisTag not in ( " ital " , " opsz " , " slnt " , " wdth " , " wght " )
and not axisTag . isupper ( )
) :
log . warning ( f " Unregistered axis tag { axisTag } should be uppercase. " )
2020-08-11 21:21:30 -07:00
axisOrder = self . expect_number_ ( )
2021-02-25 16:59:47 +00:00
self . expect_symbol_ ( " { " )
2020-08-11 21:21:30 -07:00
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 :
2021-02-25 16:59:47 +00:00
raise FeatureLibError (
f ' Expected " name " , got { self . cur_token_ } ' , self . cur_token_location_
)
2020-08-11 21:21:30 -07:00
self . expect_symbol_ ( " } " )
2021-02-25 16:59:47 +00:00
return self . ast . STATDesignAxisStatement (
axisTag , axisOrder , names , self . cur_token_location_
)
2020-08-11 21:21:30 -07:00
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 :
2021-02-25 16:59:47 +00:00
raise FeatureLibError (
f " Unexpected token { self . cur_token_ } " f " in AxisValue " ,
self . cur_token_location_ ,
)
2020-08-11 21:21:30 -07:00
self . expect_symbol_ ( " } " )
if not names :
2021-02-25 16:59:47 +00:00
raise FeatureLibError ( ' Expected " Axis Name " ' , self . cur_token_location_ )
2020-08-11 21:21:30 -07:00
if not locations :
2021-02-25 16:59:47 +00:00
raise FeatureLibError ( ' Expected " Axis location " ' , self . cur_token_location_ )
2020-08-11 21:21:30 -07:00
if len ( locations ) > 1 :
for location in locations :
if len ( location . values ) > 1 :
2021-02-25 16:59:47 +00:00
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_ ,
)
2020-08-11 21:21:30 -07:00
format4_tags = [ ]
for location in locations :
tag = location . tag
if tag in format4_tags :
2021-02-25 16:59:47 +00:00
raise FeatureLibError (
f " Axis tag { tag } already " " defined. " , self . cur_token_location_
)
2020-08-11 21:21:30 -07:00
format4_tags . append ( tag )
2021-02-25 16:59:47 +00:00
return self . ast . STATAxisValueStatement (
names , locations , flags , self . cur_token_location_
)
2020-08-11 21:21:30 -07:00
def parse_STAT_location ( self ) :
values = [ ]
tag = self . expect_tag_ ( )
if len ( tag . strip ( ) ) != 4 :
2021-02-25 16:59:47 +00:00
raise FeatureLibError (
f " Axis tag { self . cur_token_ } must be 4 " " characters " ,
self . cur_token_location_ ,
)
2020-08-11 21:21:30 -07:00
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 :
2021-02-25 16:59:47 +00:00
raise FeatureLibError (
f ' Unexpected value " { self . next_token_ } " . '
" Expected integer or float. " ,
self . next_token_location_ ,
)
2020-08-11 21:21:30 -07:00
if len ( values ) == 3 :
nominal , min_val , max_val = values
if nominal < min_val or nominal > max_val :
2021-02-25 16:59:47 +00:00
raise FeatureLibError (
f " Default value { nominal } is outside "
f " of specified range "
f " { min_val } - { max_val } . " ,
self . next_token_location_ ,
)
2021-02-19 17:17:28 -05:00
return self . ast . AxisValueLocationStatement ( tag , values )
2020-08-11 21:21:30 -07:00
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
2021-02-25 16:59:47 +00:00
raise FeatureLibError (
" DesignAxis not defined for " f " { location . tag } . " ,
self . cur_token_location_ ,
)
2020-08-11 21:21:30 -07:00
statements . append ( axisValueRecord )
self . expect_symbol_ ( " ; " )
else :
2021-02-25 16:59:47 +00:00
raise FeatureLibError (
f " Unexpected token { self . cur_token_ } " , self . cur_token_location_
)
2020-08-11 21:21:30 -07:00
elif self . cur_token_ == " ; " :
continue
2016-03-19 02:47:04 +04:00
def parse_base_tag_list_ ( self ) :
2020-05-12 23:11:17 +01:00
# Parses BASE table entries. (See `section 9.a <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.a>`_)
2020-07-15 17:16:12 +01:00
assert self . cur_token_ in (
" HorizAxis.BaseTagList " ,
" VertAxis.BaseTagList " ,
) , self . cur_token_
2016-03-19 02:47:04 +04:00
bases = [ ]
while self . next_token_ != " ; " :
bases . append ( self . expect_script_tag_ ( ) )
self . expect_symbol_ ( " ; " )
return bases
def parse_base_script_list_ ( self , count ) :
2020-07-15 17:16:12 +01:00
assert self . cur_token_ in (
" HorizAxis.BaseScriptList " ,
" VertAxis.BaseScriptList " ,
) , self . cur_token_
2016-03-19 02:47:04 +04:00
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
2015-12-04 17:10:20 +01:00
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
2016-01-25 12:13:40 +01:00
def is_next_value_ ( self ) :
return self . next_token_type_ is Lexer . NUMBER or self . next_token_ == " < "
2015-08-04 11:01:04 +02:00
def parse_valuerecord_ ( self , vertical ) :
if self . next_token_type_ is Lexer . NUMBER :
number , location = self . expect_number_ ( ) , self . cur_token_location_
if vertical :
2020-07-15 17:16:12 +01:00
val = self . ast . ValueRecord (
yAdvance = number , vertical = vertical , location = location
)
2015-08-04 11:01:04 +02:00
else :
2020-07-15 17:16:12 +01:00
val = self . ast . ValueRecord (
xAdvance = number , vertical = vertical , location = location
)
2015-08-04 11:01:04 +02:00
return val
self . expect_symbol_ ( " < " )
location = self . cur_token_location_
if self . next_token_type_ is Lexer . NAME :
name = self . expect_name_ ( )
2015-12-07 17:18:18 +01:00
if name == " NULL " :
self . expect_symbol_ ( " > " )
2019-01-19 11:46:05 +00:00
return self . ast . ValueRecord ( )
2015-08-04 11:01:04 +02:00
vrd = self . valuerecords_ . resolve ( name )
if vrd is None :
2020-07-15 17:16:12 +01:00
raise FeatureLibError (
' Unknown valueRecordDef " %s " ' % name , self . cur_token_location_
)
2015-08-04 11:01:04 +02:00
value = vrd . value
xPlacement , yPlacement = ( value . xPlacement , value . yPlacement )
xAdvance , yAdvance = ( value . xAdvance , value . yAdvance )
else :
xPlacement , yPlacement , xAdvance , yAdvance = (
2020-07-15 17:16:12 +01:00
self . expect_number_ ( ) ,
self . expect_number_ ( ) ,
self . expect_number_ ( ) ,
self . expect_number_ ( ) ,
)
2015-12-04 17:10:20 +01:00
if self . next_token_ == " < " :
2015-12-04 15:49:04 +01:00
xPlaDevice , yPlaDevice , xAdvDevice , yAdvDevice = (
2020-07-15 17:16:12 +01:00
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 ( ) )
]
)
2015-12-05 08:15:05 +00:00
if allDeltas [ 0 ] < - 128 or allDeltas [ - 1 ] > 127 :
raise FeatureLibError (
" Device value out of valid range (-128..127) " ,
2020-07-15 17:16:12 +01:00
self . cur_token_location_ ,
)
2015-12-04 15:49:04 +01:00
else :
2020-07-15 17:16:12 +01:00
xPlaDevice , yPlaDevice , xAdvDevice , yAdvDevice = ( None , None , None , None )
2015-12-04 17:10:20 +01:00
2015-08-04 11:01:04 +02:00
self . expect_symbol_ ( " > " )
2017-01-04 16:18:50 +00:00
return self . ast . ValueRecord (
2020-07-15 17:16:12 +01:00
xPlacement ,
yPlacement ,
xAdvance ,
yAdvance ,
xPlaDevice ,
yPlaDevice ,
xAdvDevice ,
yAdvDevice ,
vertical = vertical ,
location = location ,
)
2015-08-04 11:01:04 +02:00
def parse_valuerecord_definition_ ( self , vertical ) :
2020-05-12 23:11:17 +01:00
# Parses a named value record definition. (See section `2.e.v <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#2.e.v>`_)
2015-08-04 11:01:04 +02:00
assert self . is_cur_keyword_ ( " valueRecordDef " )
location = self . cur_token_location_
value = self . parse_valuerecord_ ( vertical )
name = self . expect_name_ ( )
self . expect_symbol_ ( " ; " )
2018-02-27 18:38:15 +00:00
vrd = self . ast . ValueRecordDefinition ( name , value , location = location )
2015-08-04 11:01:04 +02:00
self . valuerecords_ . define ( name , vrd )
return vrd
2015-08-01 12:35:22 +02:00
def parse_languagesystem_ ( self ) :
2015-08-01 19:58:54 +02:00
assert self . cur_token_ == " languagesystem "
2015-08-01 14:49:19 +02:00
location = self . cur_token_location_
2015-09-08 09:26:24 +02:00
script = self . expect_script_tag_ ( )
language = self . expect_language_tag_ ( )
2015-08-01 12:35:22 +02:00
self . expect_symbol_ ( " ; " )
2020-07-15 17:16:12 +01:00
return self . ast . LanguageSystemStatement ( script , language , location = location )
2015-08-01 12:35:22 +02:00
2015-08-01 19:58:54 +02:00
def parse_feature_block_ ( self ) :
assert self . cur_token_ == " feature "
location = self . cur_token_location_
tag = self . expect_tag_ ( )
2020-07-15 17:16:12 +01:00
vertical = tag in { " vkrn " , " vpal " , " vhal " , " valt " }
2018-02-05 22:57:06 -08:00
2016-03-18 09:55:51 +04:00
stylisticset = None
2018-02-05 22:57:06 -08:00
cv_feature = None
2018-02-28 23:24:02 -08:00
size_feature = False
if tag in self . SS_FEATURE_TAGS :
stylisticset = tag
elif tag in self . CV_FEATURE_TAGS :
2018-02-05 22:57:06 -08:00
cv_feature = tag
2018-02-28 23:24:02 -08:00
elif tag == " size " :
size_feature = True
2016-03-18 13:18:31 +04:00
2015-08-11 15:28:59 +02:00
use_extension = False
if self . next_token_ == " useExtension " :
self . expect_keyword_ ( " useExtension " )
use_extension = True
2020-07-15 17:16:12 +01:00
block = self . ast . FeatureBlock (
tag , use_extension = use_extension , location = location
)
self . parse_block_ ( block , vertical , stylisticset , size_feature , cv_feature )
2015-08-11 10:19:39 +02:00
return block
2015-08-03 09:37:27 +02:00
2016-01-11 16:00:52 +01:00
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_ ( " ; " )
2020-07-15 17:16:12 +01:00
return self . ast . FeatureReferenceStatement ( featureName , location = location )
2016-01-11 16:00:52 +01:00
2016-03-18 09:55:51 +04:00
def parse_featureNames_ ( self , tag ) :
2020-05-12 23:11:17 +01:00
""" Parses a ``featureNames`` statement found in stylistic set features.
See section ` 8. c < https : / / adobe - type - tools . github . io / afdko / OpenTypeFeatureFileSpecification . html #8.c>`_."""
2016-03-18 09:55:51 +04:00
assert self . cur_token_ == " featureNames " , self . cur_token_
2020-07-15 17:16:12 +01:00
block = self . ast . NestedBlock (
tag , self . cur_token_ , location = self . cur_token_location_
)
2016-03-18 09:55:51 +04:00
self . expect_symbol_ ( " { " )
for symtab in self . symbol_tables_ :
symtab . enter_scope ( )
2017-03-09 16:35:28 +01:00
while self . next_token_ != " } " or self . cur_comments_ :
self . advance_lexer_ ( comments = True )
if self . cur_token_type_ is Lexer . COMMENT :
2020-07-15 17:16:12 +01:00
block . statements . append (
self . ast . Comment ( self . cur_token_ , location = self . cur_token_location_ )
)
2017-03-09 16:35:28 +01:00
elif self . is_cur_keyword_ ( " name " ) :
location = self . cur_token_location_
platformID , platEncID , langID , string = self . parse_name_ ( )
block . statements . append (
2020-07-15 17:16:12 +01:00
self . ast . FeatureNameStatement (
tag , platformID , platEncID , langID , string , location = location
)
)
2017-03-09 16:35:28 +01:00
elif self . cur_token_ == " ; " :
continue
else :
2020-07-15 17:16:12 +01:00
raise FeatureLibError ( ' Expected " name " ' , self . cur_token_location_ )
2016-03-18 09:55:51 +04:00
self . expect_symbol_ ( " } " )
for symtab in self . symbol_tables_ :
symtab . exit_scope ( )
self . expect_symbol_ ( " ; " )
2017-03-09 16:35:28 +01:00
return block
2016-03-18 09:55:51 +04:00
2018-02-05 23:10:28 -08:00
def parse_cvParameters_ ( self , tag ) :
2020-05-12 23:11:17 +01:00
# Parses a ``cvParameters`` block found in Character Variant features.
# See section `8.d <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.d>`_.
2018-02-05 23:10:28 -08:00
assert self . cur_token_ == " cvParameters " , self . cur_token_
2020-07-15 17:16:12 +01:00
block = self . ast . NestedBlock (
tag , self . cur_token_ , location = self . cur_token_location_
)
2018-02-05 23:10:28 -08:00
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 :
2020-07-15 17:16:12 +01:00
statements . append (
self . ast . Comment ( self . cur_token_ , location = self . cur_token_location_ )
)
elif self . is_cur_keyword_ (
{
" FeatUILabelNameID " ,
" FeatUITooltipTextNameID " ,
" SampleTextNameID " ,
" ParamUILabelNameID " ,
}
) :
2018-02-05 23:10:28 -08:00
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 (
2020-07-15 17:16:12 +01:00
self . cur_token_type_ , self . cur_token_
) ,
self . cur_token_location_ ,
)
2018-02-05 23:10:28 -08:00
self . expect_symbol_ ( " } " )
for symtab in self . symbol_tables_ :
symtab . exit_scope ( )
self . expect_symbol_ ( " ; " )
return block
2018-02-05 23:21:54 -08:00
def parse_cvNameIDs_ ( self , tag , block_name ) :
assert self . cur_token_ == block_name , self . cur_token_
2020-07-15 17:16:12 +01:00
block = self . ast . NestedBlock ( tag , block_name , location = self . cur_token_location_ )
2018-02-05 23:21:54 -08:00
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 :
2020-07-15 17:16:12 +01:00
block . statements . append (
self . ast . Comment ( self . cur_token_ , location = self . cur_token_location_ )
)
2018-02-05 23:21:54 -08:00
elif self . is_cur_keyword_ ( " name " ) :
location = self . cur_token_location_
platformID , platEncID , langID , string = self . parse_name_ ( )
block . statements . append (
self . ast . CVParametersNameStatement (
2020-07-15 17:16:12 +01:00
tag ,
platformID ,
platEncID ,
langID ,
string ,
block_name ,
location = location ,
)
)
2018-02-05 23:21:54 -08:00
elif self . cur_token_ == " ; " :
continue
else :
2020-07-15 17:16:12 +01:00
raise FeatureLibError ( ' Expected " name " ' , self . cur_token_location_ )
2018-02-05 23:21:54 -08:00
self . expect_symbol_ ( " } " )
for symtab in self . symbol_tables_ :
symtab . exit_scope ( )
self . expect_symbol_ ( " ; " )
return block
2018-02-05 23:17:16 -08:00
def parse_cvCharacter_ ( self , tag ) :
assert self . cur_token_ == " Character " , self . cur_token_
2019-08-17 05:53:52 +02:00
location , character = self . cur_token_location_ , self . expect_any_number_ ( )
2018-02-05 23:17:16 -08:00
self . expect_symbol_ ( " ; " )
if not ( 0xFFFFFF > = character > = 0 ) :
2020-07-15 17:16:12 +01:00
raise FeatureLibError (
" Character value must be between "
" {:#x} and {:#x} " . format ( 0 , 0xFFFFFF ) ,
location ,
)
2018-03-01 10:39:17 +00:00
return self . ast . CharacterStatement ( character , tag , location = location )
2018-02-05 23:17:16 -08:00
2016-01-11 18:01:47 +01:00
def parse_FontRevision_ ( self ) :
2020-05-12 23:11:17 +01:00
# Parses a ``FontRevision`` statement found in the head table. See
# `section 9.c <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.c>`_.
2016-01-11 18:01:47 +01:00
assert self . cur_token_ == " FontRevision " , self . cur_token_
location , version = self . cur_token_location_ , self . expect_float_ ( )
self . expect_symbol_ ( " ; " )
if version < = 0 :
2020-07-15 17:16:12 +01:00
raise FeatureLibError ( " Font revision numbers must be positive " , location )
2018-02-27 18:38:15 +00:00
return self . ast . FontRevisionStatement ( version , location = location )
2016-01-11 18:01:47 +01:00
2020-07-15 17:16:12 +01:00
def parse_block_ (
self , block , vertical , stylisticset = None , size_feature = False , cv_feature = None
) :
2015-08-01 19:58:54 +02:00
self . expect_symbol_ ( " { " )
2015-08-03 09:37:27 +02:00
for symtab in self . symbol_tables_ :
symtab . enter_scope ( )
2015-08-04 11:01:04 +02:00
statements = block . statements
2017-03-09 14:38:51 +01:00
while self . next_token_ != " } " or self . cur_comments_ :
2017-03-08 15:19:09 +00:00
self . advance_lexer_ ( comments = True )
2017-03-08 13:50:06 +00:00
if self . cur_token_type_ is Lexer . COMMENT :
2020-07-15 17:16:12 +01:00
statements . append (
self . ast . Comment ( self . cur_token_ , location = self . cur_token_location_ )
)
2017-03-08 13:50:06 +00:00
elif self . cur_token_type_ is Lexer . GLYPHCLASS :
2015-08-04 11:01:04 +02:00
statements . append ( self . parse_glyphclass_definition_ ( ) )
2015-08-11 12:53:30 +02:00
elif self . is_cur_keyword_ ( " anchorDef " ) :
statements . append ( self . parse_anchordef_ ( ) )
2015-12-07 17:18:18 +01:00
elif self . is_cur_keyword_ ( { " enum " , " enumerate " } ) :
statements . append ( self . parse_enumerate_ ( vertical = vertical ) )
2016-01-11 16:00:52 +01:00
elif self . is_cur_keyword_ ( " feature " ) :
statements . append ( self . parse_feature_reference_ ( ) )
2015-08-05 10:41:04 +02:00
elif self . is_cur_keyword_ ( " ignore " ) :
statements . append ( self . parse_ignore_ ( ) )
2015-08-10 16:30:10 +02:00
elif self . is_cur_keyword_ ( " language " ) :
statements . append ( self . parse_language_ ( ) )
2015-08-11 10:59:26 +02:00
elif self . is_cur_keyword_ ( " lookup " ) :
statements . append ( self . parse_lookup_ ( vertical ) )
2015-12-10 13:04:16 +01:00
elif self . is_cur_keyword_ ( " lookupflag " ) :
statements . append ( self . parse_lookupflag_ ( ) )
2015-12-08 17:04:21 +01:00
elif self . is_cur_keyword_ ( " markClass " ) :
2015-12-12 12:54:23 +01:00
statements . append ( self . parse_markClass_ ( ) )
2015-12-04 11:16:43 +01:00
elif self . is_cur_keyword_ ( { " pos " , " position " } ) :
2015-12-07 17:18:18 +01:00
statements . append (
2020-07-15 17:16:12 +01:00
self . parse_position_ ( enumerated = False , vertical = vertical )
)
2015-08-10 16:30:10 +02:00
elif self . is_cur_keyword_ ( " script " ) :
statements . append ( self . parse_script_ ( ) )
2020-07-15 17:16:12 +01:00
elif self . is_cur_keyword_ ( { " sub " , " substitute " , " rsub " , " reversesub " } ) :
2015-08-04 19:55:55 +02:00
statements . append ( self . parse_substitute_ ( ) )
2015-08-11 15:14:47 +02:00
elif self . is_cur_keyword_ ( " subtable " ) :
statements . append ( self . parse_subtable_ ( ) )
2015-08-04 11:01:04 +02:00
elif self . is_cur_keyword_ ( " valueRecordDef " ) :
statements . append ( self . parse_valuerecord_definition_ ( vertical ) )
2016-03-18 09:55:51 +04:00
elif stylisticset and self . is_cur_keyword_ ( " featureNames " ) :
2017-03-09 16:35:28 +01:00
statements . append ( self . parse_featureNames_ ( stylisticset ) )
2018-02-05 22:57:06 -08:00
elif cv_feature and self . is_cur_keyword_ ( " cvParameters " ) :
statements . append ( self . parse_cvParameters_ ( cv_feature ) )
2016-03-18 13:18:31 +04:00
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_ ( ) )
2020-07-15 17:16:12 +01:00
elif (
self . cur_token_type_ is Lexer . NAME
and self . cur_token_ in self . extensions
) :
2017-01-04 16:18:50 +00:00
statements . append ( self . extensions [ self . cur_token_ ] ( self ) )
2016-01-21 10:30:16 -08:00
elif self . cur_token_ == " ; " :
continue
2015-08-01 19:58:54 +02:00
else :
2015-08-21 17:09:46 +02:00
raise FeatureLibError (
2020-07-15 17:16:12 +01:00
" Expected glyph class definition or statement: got {} {} " . format (
self . cur_token_type_ , self . cur_token_
) ,
self . cur_token_location_ ,
)
2015-08-01 19:58:54 +02:00
self . expect_symbol_ ( " } " )
2015-08-03 09:37:27 +02:00
for symtab in self . symbol_tables_ :
symtab . exit_scope ( )
2015-08-11 10:19:39 +02:00
name = self . expect_name_ ( )
if name != block . name . strip ( ) :
2020-07-15 17:16:12 +01:00
raise FeatureLibError (
' Expected " %s " ' % block . name . strip ( ) , self . cur_token_location_
)
2015-08-01 19:58:54 +02:00
self . expect_symbol_ ( " ; " )
2017-12-21 02:28:23 +02:00
# 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 ) :
2019-02-19 04:42:28 +02:00
has_multiple = not any ( [ s . prefix , s . suffix , s . forceChain ] )
2017-12-21 02:28:23 +02:00
# Upgrade all single substitutions to multiple substitutions.
if has_single and has_multiple :
2020-01-29 22:34:25 +02:00
statements = [ ]
for s in block . statements :
2017-12-21 02:28:23 +02:00
if isinstance ( s , self . ast . SingleSubstStatement ) :
2020-01-29 22:34:25 +02:00
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 (
2020-07-15 17:16:12 +01:00
s . prefix ,
glyph ,
s . suffix ,
[ replacements [ i ] ] ,
s . forceChain ,
location = s . location ,
)
)
2020-01-29 22:34:25 +02:00
else :
statements . append ( s )
block . statements = statements
2017-12-21 02:28:23 +02:00
2015-08-01 17:34:02 +02:00
def is_cur_keyword_ ( self , k ) :
2015-12-03 13:05:42 +01:00
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
2015-08-01 12:35:22 +02:00
2015-12-08 19:04:42 +01:00
def expect_class_name_ ( self ) :
2015-12-08 17:04:21 +01:00
self . advance_lexer_ ( )
if self . cur_token_type_ is not Lexer . GLYPHCLASS :
raise FeatureLibError ( " Expected @NAME " , self . cur_token_location_ )
return self . cur_token_
2016-01-12 10:58:00 +01:00
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_ )
2018-01-22 18:27:36 +00:00
def expect_filename_ ( self ) :
self . advance_lexer_ ( )
if self . cur_token_type_ is not Lexer . FILENAME :
2020-07-15 17:16:12 +01:00
raise FeatureLibError ( " Expected file name " , self . cur_token_location_ )
2018-01-22 18:27:36 +00:00
return self . cur_token_
2016-01-12 10:58:00 +01:00
def expect_glyph_ ( self ) :
self . advance_lexer_ ( )
if self . cur_token_type_ is Lexer . NAME :
2016-11-02 15:59:24 +00:00
self . cur_token_ = self . cur_token_ . lstrip ( " \\ " )
2016-01-13 16:31:05 +01:00
if len ( self . cur_token_ ) > 63 :
raise FeatureLibError (
" Glyph names must not be longer than 63 characters " ,
2020-07-15 17:16:12 +01:00
self . cur_token_location_ ,
)
2016-01-12 10:58:00 +01:00
return self . cur_token_
elif self . cur_token_type_ is Lexer . CID :
return " cid %05d " % self . cur_token_
2020-07-15 17:16:12 +01:00
raise FeatureLibError ( " Expected a glyph name or CID " , self . cur_token_location_ )
2016-01-12 10:58:00 +01:00
2020-02-13 14:47:29 +00:00
def check_glyph_name_in_glyph_set ( self , * names ) :
""" Raises if glyph name (just `start`) or glyph names of a
range ( ` start ` and ` end ` ) are not in the glyph set .
If no glyph set is present , does nothing .
"""
if self . glyphNames_ :
missing = [ name for name in names if name not in self . glyphNames_ ]
if missing :
raise FeatureLibError (
" The following glyph names are referenced but are missing from the "
f " glyph set: { ' , ' . join ( missing ) } " ,
2020-07-15 17:16:12 +01:00
self . cur_token_location_ ,
2020-02-13 14:47:29 +00:00
)
2015-12-08 19:04:42 +01:00
def expect_markClass_reference_ ( self ) :
name = self . expect_class_name_ ( )
2015-12-12 12:54:23 +01:00
mc = self . glyphclasses_ . resolve ( name )
if mc is None :
2020-07-15 17:16:12 +01:00
raise FeatureLibError (
" Unknown markClass @ %s " % name , self . cur_token_location_
)
2017-01-04 16:18:50 +00:00
if not isinstance ( mc , self . ast . MarkClass ) :
2020-07-15 17:16:12 +01:00
raise FeatureLibError (
" @ %s is not a markClass " % name , self . cur_token_location_
)
2015-12-12 12:54:23 +01:00
return mc
2015-12-08 19:04:42 +01:00
2015-08-01 12:35:22 +02:00
def expect_tag_ ( self ) :
self . advance_lexer_ ( )
if self . cur_token_type_ is not Lexer . NAME :
2015-08-21 17:09:46 +02:00
raise FeatureLibError ( " Expected a tag " , self . cur_token_location_ )
2015-08-01 12:35:22 +02:00
if len ( self . cur_token_ ) > 4 :
2020-07-15 17:16:12 +01:00
raise FeatureLibError (
2021-02-19 17:17:28 -05:00
" Tags cannot be longer than 4 characters " , self . cur_token_location_
2020-07-15 17:16:12 +01:00
)
2015-08-01 12:35:22 +02:00
return ( self . cur_token_ + " " ) [ : 4 ]
2015-09-08 09:26:24 +02:00
def expect_script_tag_ ( self ) :
tag = self . expect_tag_ ( )
if tag == " dflt " :
raise FeatureLibError (
' " dflt " is not a valid script tag; use " DFLT " instead ' ,
2020-07-15 17:16:12 +01:00
self . cur_token_location_ ,
)
2015-09-08 09:26:24 +02:00
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 ' ,
2020-07-15 17:16:12 +01:00
self . cur_token_location_ ,
)
2015-09-08 09:26:24 +02:00
return tag
2015-08-01 12:35:22 +02:00
def expect_symbol_ ( self , symbol ) :
self . advance_lexer_ ( )
if self . cur_token_type_ is Lexer . SYMBOL and self . cur_token_ == symbol :
return symbol
2020-07-15 17:16:12 +01:00
raise FeatureLibError ( " Expected ' %s ' " % symbol , self . cur_token_location_ )
2015-08-01 12:35:22 +02:00
2015-08-05 10:41:04 +02:00
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_
2020-07-15 17:16:12 +01:00
raise FeatureLibError ( ' Expected " %s " ' % keyword , self . cur_token_location_ )
2015-08-05 10:41:04 +02:00
2015-08-01 17:34:02 +02:00
def expect_name_ ( self ) :
self . advance_lexer_ ( )
if self . cur_token_type_ is Lexer . NAME :
return self . cur_token_
2015-08-21 17:09:46 +02:00
raise FeatureLibError ( " Expected a name " , self . cur_token_location_ )
2015-08-01 17:34:02 +02:00
2015-08-04 11:01:04 +02:00
def expect_number_ ( self ) :
self . advance_lexer_ ( )
if self . cur_token_type_ is Lexer . NUMBER :
return self . cur_token_
2015-08-21 17:09:46 +02:00
raise FeatureLibError ( " Expected a number " , self . cur_token_location_ )
2015-08-04 11:01:04 +02:00
2019-08-17 05:53:52 +02:00
def expect_any_number_ ( self ) :
self . advance_lexer_ ( )
if self . cur_token_type_ in Lexer . NUMBERS :
return self . cur_token_
2020-07-15 17:16:12 +01:00
raise FeatureLibError (
" Expected a decimal, hexadecimal or octal number " , self . cur_token_location_
)
2019-08-17 05:53:52 +02:00
2016-01-11 18:01:47 +01:00
def expect_float_ ( self ) :
self . advance_lexer_ ( )
if self . cur_token_type_ is Lexer . FLOAT :
return self . cur_token_
2020-07-15 17:16:12 +01:00
raise FeatureLibError (
" Expected a floating-point number " , self . cur_token_location_
)
2016-01-11 18:01:47 +01:00
2016-03-18 13:18:31 +04:00
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 :
2020-07-15 17:16:12 +01:00
raise FeatureLibError (
2021-02-25 16:59:47 +00:00
" Expected an integer or floating-point number " , self . cur_token_location_
2020-08-11 21:21:30 -07:00
)
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 (
2021-02-25 16:59:47 +00:00
f " Unexpected STAT flag { self . cur_token_ } " , self . cur_token_location_
2020-08-11 21:21:30 -07:00
)
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 (
2021-02-25 16:59:47 +00:00
" Expected an integer or floating-point number " , self . cur_token_location_
2020-07-15 17:16:12 +01:00
)
2016-03-18 13:18:31 +04:00
2016-03-14 23:44:25 +04:00
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_ )
2017-03-09 14:38:51 +01:00
def advance_lexer_ ( self , comments = False ) :
if comments and self . cur_comments_ :
2017-03-08 13:50:06 +00:00
self . cur_token_type_ = Lexer . COMMENT
self . cur_token_ , self . cur_token_location_ = self . cur_comments_ . pop ( 0 )
return
2017-03-08 14:15:34 +00:00
else :
2017-03-08 13:50:06 +00:00
self . cur_token_type_ , self . cur_token_ , self . cur_token_location_ = (
2020-07-15 17:16:12 +01:00
self . next_token_type_ ,
self . next_token_ ,
self . next_token_location_ ,
)
2017-03-08 14:05:00 +00:00
while True :
2017-03-08 13:50:06 +00:00
try :
2020-07-15 17:16:12 +01:00
(
self . next_token_type_ ,
self . next_token_ ,
self . next_token_location_ ,
) = next ( self . lexer_ )
2017-03-08 13:50:06 +00:00
except StopIteration :
self . next_token_type_ , self . next_token_ = ( None , None )
2017-03-08 14:05:00 +00:00
if self . next_token_type_ != Lexer . COMMENT :
break
2017-03-08 13:50:06 +00:00
self . cur_comments_ . append ( ( self . next_token_ , self . next_token_location_ ) )
2015-08-01 17:34:02 +02:00
2015-09-04 16:22:16 +02:00
@staticmethod
def reverse_string_ ( s ) :
2015-09-10 07:13:10 +02:00
""" ' abc ' --> ' cba ' """
2020-07-15 17:16:12 +01:00
return " " . join ( reversed ( list ( s ) ) )
2015-09-04 16:22:16 +02:00
2016-01-12 10:58:00 +01:00
def make_cid_range_ ( self , location , start , limit ) :
2016-04-21 10:46:52 +01:00
""" (location, 999, 1001) --> [ " cid00999 " , " cid01000 " , " cid01001 " ] """
result = list ( )
2016-01-12 10:58:00 +01:00
if start > limit :
raise FeatureLibError (
2020-07-15 17:16:12 +01:00
" Bad range: start should be less than limit " , location
)
2016-01-12 10:58:00 +01:00
for cid in range ( start , limit + 1 ) :
2016-04-21 10:46:52 +01:00
result . append ( " cid %05d " % cid )
2016-01-12 10:58:00 +01:00
return result
2015-08-01 17:34:02 +02:00
def make_glyph_range_ ( self , location , start , limit ) :
2016-04-21 10:46:52 +01:00
""" (location, " a.sc " , " d.sc " ) --> [ " a.sc " , " b.sc " , " c.sc " , " d.sc " ] """
result = list ( )
2015-08-01 17:34:02 +02:00
if len ( start ) != len ( limit ) :
2015-08-21 17:09:46 +02:00
raise FeatureLibError (
2020-07-15 17:16:12 +01:00
' Bad range: " %s " and " %s " should have the same length ' % ( start , limit ) ,
location ,
)
2015-09-04 16:22:16 +02:00
rev = self . reverse_string_
2015-08-01 17:34:02 +02:00
prefix = os . path . commonprefix ( [ start , limit ] )
suffix = rev ( os . path . commonprefix ( [ rev ( start ) , rev ( limit ) ] ) )
if len ( suffix ) > 0 :
2020-07-15 17:16:12 +01:00
start_range = start [ len ( prefix ) : - len ( suffix ) ]
limit_range = limit [ len ( prefix ) : - len ( suffix ) ]
2015-08-01 17:34:02 +02:00
else :
2020-07-15 17:16:12 +01:00
start_range = start [ len ( prefix ) : ]
limit_range = limit [ len ( prefix ) : ]
2015-08-01 17:34:02 +02:00
if start_range > = limit_range :
2015-08-21 17:09:46 +02:00
raise FeatureLibError (
2020-07-15 17:16:12 +01:00
" Start of range must be smaller than its end " , location
)
2015-08-01 17:34:02 +02:00
2020-07-15 17:16:12 +01:00
uppercase = re . compile ( r " ^[A-Z]$ " )
2015-08-01 17:34:02 +02:00
if uppercase . match ( start_range ) and uppercase . match ( limit_range ) :
for c in range ( ord ( start_range ) , ord ( limit_range ) + 1 ) :
2016-04-21 10:46:52 +01:00
result . append ( " %s %c %s " % ( prefix , c , suffix ) )
2015-08-01 17:34:02 +02:00
return result
2020-07-15 17:16:12 +01:00
lowercase = re . compile ( r " ^[a-z]$ " )
2015-08-01 17:34:02 +02:00
if lowercase . match ( start_range ) and lowercase . match ( limit_range ) :
for c in range ( ord ( start_range ) , ord ( limit_range ) + 1 ) :
2016-04-21 10:46:52 +01:00
result . append ( " %s %c %s " % ( prefix , c , suffix ) )
2015-08-01 17:34:02 +02:00
return result
2020-07-15 17:16:12 +01:00
digits = re . compile ( r " ^[0-9] { 1,3}$ " )
2015-08-01 17:34:02 +02:00
if digits . match ( start_range ) and digits . match ( limit_range ) :
for i in range ( int ( start_range , 10 ) , int ( limit_range , 10 ) + 1 ) :
2020-07-15 17:16:12 +01:00
number = ( " 000 " + str ( i ) ) [ - len ( start_range ) : ]
2016-04-21 10:46:52 +01:00
result . append ( " %s %s %s " % ( prefix , number , suffix ) )
2015-08-01 17:34:02 +02:00
return result
2020-07-15 17:16:12 +01:00
raise FeatureLibError ( ' Bad range: " %s - %s " ' % ( start , limit ) , location )
2015-08-03 09:37:27 +02:00
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