diff options
Diffstat (limited to 'Lib/fontTools/feaLib/parser.py')
-rw-r--r-- | Lib/fontTools/feaLib/parser.py | 203 |
1 files changed, 171 insertions, 32 deletions
diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py index 804cba9f..fd53573d 100644 --- a/Lib/fontTools/feaLib/parser.py +++ b/Lib/fontTools/feaLib/parser.py @@ -1,7 +1,8 @@ from fontTools.feaLib.error import FeatureLibError from fontTools.feaLib.lexer import Lexer, IncludingLexer, NonIncludingLexer +from fontTools.feaLib.variableScalar import VariableScalar from fontTools.misc.encodingTools import getEncoding -from fontTools.misc.py23 import bytechr, tobytes, tostr +from fontTools.misc.textTools import bytechr, tobytes, tostr import fontTools.feaLib.ast as ast import logging import os @@ -101,6 +102,10 @@ class Parser(object): statements.append(self.parse_markClass_()) elif self.is_cur_keyword_("feature"): statements.append(self.parse_feature_block_()) + elif self.is_cur_keyword_("conditionset"): + statements.append(self.parse_conditionset_()) + elif self.is_cur_keyword_("variation"): + statements.append(self.parse_feature_block_(variation=True)) elif self.is_cur_keyword_("table"): statements.append(self.parse_table_()) elif self.is_cur_keyword_("valueRecordDef"): @@ -152,7 +157,7 @@ class Parser(object): location=location, ) - x, y = self.expect_number_(), self.expect_number_() + x, y = self.expect_number_(variable=True), self.expect_number_(variable=True) contourpoint = None if self.next_token_ == "contourpoint": # Format B @@ -380,8 +385,7 @@ class Parser(object): self.expect_symbol_("-") range_end = self.expect_cid_() self.check_glyph_name_in_glyph_set( - f"cid{range_start:05d}", - f"cid{range_end:05d}", + f"cid{range_start:05d}", f"cid{range_end:05d}", ) glyphs.add_cid_range( range_start, @@ -473,14 +477,38 @@ class Parser(object): assert lookups == [] return ([], prefix, [None] * len(prefix), values, [], hasMarks) else: - 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 - ) + if any(values[: len(prefix)]): + raise FeatureLibError( + "Positioning cannot be applied in the bactrack glyph sequence, " + "before the marked glyph sequence.", + self.cur_token_location_, + ) + marked_values = values[len(prefix) : len(prefix) + len(glyphs)] + if any(marked_values): + if any(values[len(prefix) + len(glyphs) :]): + raise FeatureLibError( + "Positioning values are allowed only in the marked glyph " + "sequence, or after the final glyph node when only one glyph " + "node is marked.", + self.cur_token_location_, + ) + values = marked_values + elif values and values[-1]: + if len(glyphs) > 1 or any(values[:-1]): + raise FeatureLibError( + "Positioning values are allowed only in the marked glyph " + "sequence, or after the final glyph node when only one glyph " + "node is marked.", + self.cur_token_location_, + ) + values = values[-1:] + elif any(values): + raise FeatureLibError( + "Positioning values are allowed only in the marked glyph " + "sequence, or after the final glyph node when only one glyph " + "node is marked.", + self.cur_token_location_, + ) return (prefix, glyphs, lookups, values, suffix, hasMarks) def parse_chain_context_(self): @@ -656,6 +684,8 @@ class Parser(object): assert self.is_cur_keyword_("markClass") location = self.cur_token_location_ glyphs = self.parse_glyphclass_(accept_glyphname=True) + if not glyphs.glyphSet(): + raise FeatureLibError("Empty glyph class in mark class definition", location) anchor = self.parse_anchor_() name = self.expect_class_name_() self.expect_symbol_(";") @@ -844,7 +874,7 @@ class Parser(object): num_lookups = len([l for l in lookups if l is not None]) is_deletion = False - if len(new) == 1 and len(new[0].glyphSet()) == 0: + if len(new) == 1 and isinstance(new[0], ast.NullGlyph): new = [] # Deletion is_deletion = True @@ -868,18 +898,31 @@ class Parser(object): old, new, old_prefix, old_suffix, forceChain=hasMarks, location=location ) + # Glyph deletion, built as GSUB lookup type 2: Multiple substitution + # with empty replacement. + if is_deletion and len(old) == 1 and num_lookups == 0: + return self.ast.MultipleSubstStatement( + old_prefix, + old[0], + old_suffix, + (), + forceChain=hasMarks, + location=location, + ) + # GSUB lookup type 2: Multiple substitution. # Format: "substitute f_f_i by f f i;" if ( not reverse and len(old) == 1 and len(old[0].glyphSet()) == 1 - and ( - (len(new) > 1 and max([len(n.glyphSet()) for n in new]) == 1) - or len(new) == 0 - ) + and len(new) > 1 + and max([len(n.glyphSet()) for n in new]) == 1 and num_lookups == 0 ): + for n in new: + if not list(n.glyphSet()): + raise FeatureLibError("Empty class in replacement", location) return self.ast.MultipleSubstStatement( old_prefix, tuple(old[0].glyphSet())[0], @@ -971,8 +1014,8 @@ class Parser(object): location = self.cur_token_location_ DesignSize = self.expect_decipoint_() SubfamilyID = self.expect_number_() - RangeStart = 0. - RangeEnd = 0. + RangeStart = 0.0 + RangeEnd = 0.0 if self.next_token_type_ in (Lexer.NUMBER, Lexer.FLOAT) or SubfamilyID != 0: RangeStart = self.expect_decipoint_() RangeEnd = self.expect_decipoint_() @@ -1551,11 +1594,20 @@ class Parser(object): return result def is_next_value_(self): - return self.next_token_type_ is Lexer.NUMBER or self.next_token_ == "<" + return ( + self.next_token_type_ is Lexer.NUMBER + or self.next_token_ == "<" + or self.next_token_ == "(" + ) def parse_valuerecord_(self, vertical): - if self.next_token_type_ is Lexer.NUMBER: - number, location = self.expect_number_(), self.cur_token_location_ + if ( + self.next_token_type_ is Lexer.SYMBOL and self.next_token_ == "(" + ) or self.next_token_type_ is Lexer.NUMBER: + number, location = ( + self.expect_number_(variable=True), + self.cur_token_location_, + ) if vertical: val = self.ast.ValueRecord( yAdvance=number, vertical=vertical, location=location @@ -1582,10 +1634,10 @@ class Parser(object): xAdvance, yAdvance = (value.xAdvance, value.yAdvance) else: xPlacement, yPlacement, xAdvance, yAdvance = ( - self.expect_number_(), - self.expect_number_(), - self.expect_number_(), - self.expect_number_(), + self.expect_number_(variable=True), + self.expect_number_(variable=True), + self.expect_number_(variable=True), + self.expect_number_(variable=True), ) if self.next_token_ == "<": @@ -1645,8 +1697,11 @@ class Parser(object): self.expect_symbol_(";") return self.ast.LanguageSystemStatement(script, language, location=location) - def parse_feature_block_(self): - assert self.cur_token_ == "feature" + def parse_feature_block_(self, variation=False): + if variation: + assert self.cur_token_ == "variation" + else: + assert self.cur_token_ == "feature" location = self.cur_token_location_ tag = self.expect_tag_() vertical = tag in {"vkrn", "vpal", "vhal", "valt"} @@ -1661,14 +1716,22 @@ class Parser(object): elif tag == "size": size_feature = True + if variation: + conditionset = self.expect_name_() + use_extension = False if self.next_token_ == "useExtension": self.expect_keyword_("useExtension") use_extension = True - block = self.ast.FeatureBlock( - tag, use_extension=use_extension, location=location - ) + if variation: + block = self.ast.VariationBlock( + tag, conditionset, use_extension=use_extension, location=location + ) + else: + block = self.ast.FeatureBlock( + tag, use_extension=use_extension, location=location + ) self.parse_block_(block, vertical, stylisticset, size_feature, cv_feature) return block @@ -1816,6 +1879,43 @@ class Parser(object): raise FeatureLibError("Font revision numbers must be positive", location) return self.ast.FontRevisionStatement(version, location=location) + def parse_conditionset_(self): + name = self.expect_name_() + + conditions = {} + self.expect_symbol_("{") + + while self.next_token_ != "}": + self.advance_lexer_() + if self.cur_token_type_ is not Lexer.NAME: + raise FeatureLibError("Expected an axis name", self.cur_token_location_) + + axis = self.cur_token_ + if axis in conditions: + raise FeatureLibError( + f"Repeated condition for axis {axis}", self.cur_token_location_ + ) + + if self.next_token_type_ is Lexer.FLOAT: + min_value = self.expect_float_() + elif self.next_token_type_ is Lexer.NUMBER: + min_value = self.expect_number_(variable=False) + + if self.next_token_type_ is Lexer.FLOAT: + max_value = self.expect_float_() + elif self.next_token_type_ is Lexer.NUMBER: + max_value = self.expect_number_(variable=False) + self.expect_symbol_(";") + + conditions[axis] = (min_value, max_value) + + self.expect_symbol_("}") + + finalname = self.expect_name_() + if finalname != name: + raise FeatureLibError('Expected "%s"' % name, self.cur_token_location_) + return self.ast.ConditionsetStatement(name, conditions) + def parse_block_( self, block, vertical, stylisticset=None, size_feature=False, cv_feature=None ): @@ -2046,12 +2146,51 @@ class Parser(object): return self.cur_token_ raise FeatureLibError("Expected a name", self.cur_token_location_) - def expect_number_(self): + def expect_number_(self, variable=False): self.advance_lexer_() if self.cur_token_type_ is Lexer.NUMBER: return self.cur_token_ + if variable and self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == "(": + return self.expect_variable_scalar_() raise FeatureLibError("Expected a number", self.cur_token_location_) + def expect_variable_scalar_(self): + self.advance_lexer_() # "(" + scalar = VariableScalar() + while True: + if self.cur_token_type_ == Lexer.SYMBOL and self.cur_token_ == ")": + break + location, value = self.expect_master_() + scalar.add_value(location, value) + return scalar + + def expect_master_(self): + location = {} + while True: + if self.cur_token_type_ is not Lexer.NAME: + raise FeatureLibError("Expected an axis name", self.cur_token_location_) + axis = self.cur_token_ + self.advance_lexer_() + if not (self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == "="): + raise FeatureLibError( + "Expected an equals sign", self.cur_token_location_ + ) + value = self.expect_number_() + location[axis] = value + if self.next_token_type_ is Lexer.NAME and self.next_token_[0] == ":": + # Lexer has just read the value as a glyph name. We'll correct it later + break + self.advance_lexer_() + if not (self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == ","): + raise FeatureLibError( + "Expected an comma or an equals sign", self.cur_token_location_ + ) + self.advance_lexer_() + self.advance_lexer_() + value = int(self.cur_token_[1:]) + self.advance_lexer_() + return location, value + def expect_any_number_(self): self.advance_lexer_() if self.cur_token_type_ in Lexer.NUMBERS: |