aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/feaLib/parser.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/fontTools/feaLib/parser.py')
-rw-r--r--Lib/fontTools/feaLib/parser.py203
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: