aboutsummaryrefslogtreecommitdiff
path: root/Tests/feaLib
diff options
context:
space:
mode:
Diffstat (limited to 'Tests/feaLib')
-rw-r--r--Tests/feaLib/builder_test.py531
-rw-r--r--Tests/feaLib/data/PairPosSubtable.fea4
-rw-r--r--Tests/feaLib/data/bug2276.fea11
-rw-r--r--Tests/feaLib/data/bug2276.ttx59
-rw-r--r--Tests/feaLib/data/delete_glyph.fea4
-rw-r--r--Tests/feaLib/data/delete_glyph.ttx15
-rw-r--r--Tests/feaLib/data/variable_conditionset.fea13
-rw-r--r--Tests/feaLib/data/variable_conditionset.ttx67
-rw-r--r--Tests/feaLib/data/variable_scalar_anchor.fea4
-rw-r--r--Tests/feaLib/data/variable_scalar_anchor.ttx101
-rw-r--r--Tests/feaLib/data/variable_scalar_valuerecord.fea5
-rw-r--r--Tests/feaLib/data/variable_scalar_valuerecord.ttx104
-rw-r--r--Tests/feaLib/lexer_test.py2
-rw-r--r--Tests/feaLib/parser_test.py944
14 files changed, 1309 insertions, 555 deletions
diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py
index 0a55239c..5c298e85 100644
--- a/Tests/feaLib/builder_test.py
+++ b/Tests/feaLib/builder_test.py
@@ -1,11 +1,15 @@
from fontTools.misc.loggingTools import CapturingLogHandler
-from fontTools.feaLib.builder import Builder, addOpenTypeFeatures, \
- addOpenTypeFeaturesFromString
+from fontTools.feaLib.builder import (
+ Builder,
+ addOpenTypeFeatures,
+ addOpenTypeFeaturesFromString,
+)
from fontTools.feaLib.error import FeatureLibError
-from fontTools.ttLib import TTFont
+from fontTools.ttLib import TTFont, newTable
from fontTools.feaLib.parser import Parser
from fontTools.feaLib import ast
from fontTools.feaLib.lexer import Lexer
+from fontTools.fontBuilder import addFvar
import difflib
from io import StringIO
import os
@@ -65,7 +69,7 @@ class BuilderTest(unittest.TestCase):
spec9a spec9b spec9c1 spec9c2 spec9c3 spec9d spec9e spec9f spec9g
spec10
bug453 bug457 bug463 bug501 bug502 bug504 bug505 bug506 bug509
- bug512 bug514 bug568 bug633 bug1307 bug1459
+ bug512 bug514 bug568 bug633 bug1307 bug1459 bug2276
name size size2 multiple_feature_blocks omitted_GlyphClassDef
ZeroValue_SinglePos_horizontal ZeroValue_SinglePos_vertical
ZeroValue_PairPos_horizontal ZeroValue_PairPos_vertical
@@ -75,8 +79,14 @@ class BuilderTest(unittest.TestCase):
SingleSubstSubtable aalt_chain_contextual_subst AlternateChained
MultipleLookupsPerGlyph MultipleLookupsPerGlyph2 GSUB_6_formats
GSUB_5_formats delete_glyph STAT_test STAT_test_elidedFallbackNameID
+ variable_scalar_valuerecord variable_scalar_anchor variable_conditionset
""".split()
+ VARFONT_AXES = [
+ ("wght", 200, 200, 1000, "Weight"),
+ ("wdth", 100, 100, 200, "Width"),
+ ]
+
def __init__(self, methodName):
unittest.TestCase.__init__(self, methodName)
# Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
@@ -101,25 +111,36 @@ class BuilderTest(unittest.TestCase):
if not self.tempdir:
self.tempdir = tempfile.mkdtemp()
self.num_tempfiles += 1
- return os.path.join(self.tempdir,
- "tmp%d%s" % (self.num_tempfiles, suffix))
+ return os.path.join(self.tempdir, "tmp%d%s" % (self.num_tempfiles, suffix))
def read_ttx(self, path):
lines = []
with open(path, "r", encoding="utf-8") as ttx:
for line in ttx.readlines():
- # Elide ttFont attributes because ttLibVersion may change,
- # and use os-native line separators so we can run difflib.
+ # Elide ttFont attributes because ttLibVersion may change.
if line.startswith("<ttFont "):
- lines.append("<ttFont>" + os.linesep)
+ lines.append("<ttFont>\n")
else:
- lines.append(line.rstrip() + os.linesep)
+ lines.append(line.rstrip() + "\n")
return lines
def expect_ttx(self, font, expected_ttx, replace=None):
path = self.temp_path(suffix=".ttx")
- font.saveXML(path, tables=['head', 'name', 'BASE', 'GDEF', 'GSUB',
- 'GPOS', 'OS/2', 'STAT', 'hhea', 'vhea'])
+ font.saveXML(
+ path,
+ tables=[
+ "head",
+ "name",
+ "BASE",
+ "GDEF",
+ "GSUB",
+ "GPOS",
+ "OS/2",
+ "STAT",
+ "hhea",
+ "vhea",
+ ],
+ )
actual = self.read_ttx(path)
expected = self.read_ttx(expected_ttx)
if replace:
@@ -128,7 +149,8 @@ class BuilderTest(unittest.TestCase):
expected[i] = expected[i].replace(k, v)
if actual != expected:
for line in difflib.unified_diff(
- expected, actual, fromfile=expected_ttx, tofile=path):
+ expected, actual, fromfile=expected_ttx, tofile=path
+ ):
sys.stderr.write(line)
self.fail("TTX output is different from expected")
@@ -139,13 +161,17 @@ class BuilderTest(unittest.TestCase):
def check_feature_file(self, name):
font = makeTTFont()
+ if name.startswith("variable_"):
+ font["name"] = newTable("name")
+ addFvar(font, self.VARFONT_AXES, [])
+ del font["name"]
feapath = self.getpath("%s.fea" % name)
addOpenTypeFeatures(font, feapath)
self.expect_ttx(font, self.getpath("%s.ttx" % name))
# Check that:
# 1) tables do compile (only G* tables as long as we have a mock font)
# 2) dumping after save-reload yields the same TTX dump as before
- for tag in ('GDEF', 'GSUB', 'GPOS'):
+ for tag in ("GDEF", "GSUB", "GPOS"):
if tag in font:
data = font[tag].compile(font)
font[tag].decompile(data, font)
@@ -154,11 +180,11 @@ class BuilderTest(unittest.TestCase):
debugttx = self.getpath("%s-debug.ttx" % name)
if os.path.exists(debugttx):
addOpenTypeFeatures(font, feapath, debug=True)
- self.expect_ttx(font, debugttx, replace = {"__PATH__": feapath})
+ self.expect_ttx(font, debugttx, replace={"__PATH__": feapath})
def check_fea2fea_file(self, name, base=None, parser=Parser):
font = makeTTFont()
- fname = (name + ".fea") if '.' not in name else name
+ fname = (name + ".fea") if "." not in name else name
p = parser(self.getpath(fname), glyphNames=font.getGlyphOrder())
doc = p.parse()
actual = self.normal_fea(doc.asFea().split("\n"))
@@ -168,12 +194,16 @@ class BuilderTest(unittest.TestCase):
if expected != actual:
fname = name.rsplit(".", 1)[0] + ".fea"
for line in difflib.unified_diff(
- expected, actual,
- fromfile=fname + " (expected)",
- tofile=fname + " (actual)"):
- sys.stderr.write(line+"\n")
- self.fail("Fea2Fea output is different from expected. "
- "Generated:\n{}\n".format("\n".join(actual)))
+ expected,
+ actual,
+ fromfile=fname + " (expected)",
+ tofile=fname + " (actual)",
+ ):
+ sys.stderr.write(line + "\n")
+ self.fail(
+ "Fea2Fea output is different from expected. "
+ "Generated:\n{}\n".format("\n".join(actual))
+ )
def normal_fea(self, lines):
output = []
@@ -198,13 +228,14 @@ class BuilderTest(unittest.TestCase):
def test_alternateSubst_multipleSubstitutionsForSameGlyph(self):
self.assertRaisesRegex(
FeatureLibError,
- "Already defined alternates for glyph \"A\"",
+ 'Already defined alternates for glyph "A"',
self.build,
"feature test {"
" sub A from [A.alt1 A.alt2];"
" sub B from [B.alt1 B.alt2 B.alt3];"
" sub A from [A.alt1 A.alt2];"
- "} test;")
+ "} test;",
+ )
def test_singleSubst_multipleIdenticalSubstitutionsForSameGlyph_info(self):
logger = logging.getLogger("fontTools.feaLib.builder")
@@ -214,19 +245,23 @@ class BuilderTest(unittest.TestCase):
" sub A by A.sc;"
" sub B by B.sc;"
" sub A by A.sc;"
- "} test;")
- captor.assertRegex('Removing duplicate single substitution from glyph "A" to "A.sc"')
+ "} test;"
+ )
+ captor.assertRegex(
+ 'Removing duplicate single substitution from glyph "A" to "A.sc"'
+ )
def test_multipleSubst_multipleSubstitutionsForSameGlyph(self):
self.assertRaisesRegex(
FeatureLibError,
- "Already defined substitution for glyph \"f_f_i\"",
+ 'Already defined substitution for glyph "f_f_i"',
self.build,
"feature test {"
" sub f_f_i by f f i;"
" sub c_t by c t;"
" sub f_f_i by f_f i;"
- "} test;")
+ "} test;",
+ )
def test_multipleSubst_multipleIdenticalSubstitutionsForSameGlyph_info(self):
logger = logging.getLogger("fontTools.feaLib.builder")
@@ -236,8 +271,11 @@ class BuilderTest(unittest.TestCase):
" sub f_f_i by f f i;"
" sub c_t by c t;"
" sub f_f_i by f f i;"
- "} test;")
- captor.assertRegex(r"Removing duplicate multiple substitution from glyph \"f_f_i\" to \('f', 'f', 'i'\)")
+ "} test;"
+ )
+ captor.assertRegex(
+ r"Removing duplicate multiple substitution from glyph \"f_f_i\" to \('f', 'f', 'i'\)"
+ )
def test_pairPos_redefinition_warning(self):
# https://github.com/fonttools/fonttools/issues/1147
@@ -251,17 +289,18 @@ class BuilderTest(unittest.TestCase):
" pos yacute semicolon -70;"
" enum pos @Y_LC semicolon -80;"
" pos @Y_LC @SMALL_PUNC -100;"
- "} kern;")
+ "} kern;"
+ )
captor.assertRegex("Already defined position for pair yacute semicolon")
# the first definition prevails: yacute semicolon -70
st = font["GPOS"].table.LookupList.Lookup[0].SubTable[0]
self.assertEqual(st.Coverage.glyphs[2], "yacute")
- self.assertEqual(st.PairSet[2].PairValueRecord[0].SecondGlyph,
- "semicolon")
- self.assertEqual(vars(st.PairSet[2].PairValueRecord[0].Value1),
- {"XAdvance": -70})
+ self.assertEqual(st.PairSet[2].PairValueRecord[0].SecondGlyph, "semicolon")
+ self.assertEqual(
+ vars(st.PairSet[2].PairValueRecord[0].Value1), {"XAdvance": -70}
+ )
def test_singleSubst_multipleSubstitutionsForSameGlyph(self):
self.assertRaisesRegex(
@@ -271,127 +310,153 @@ class BuilderTest(unittest.TestCase):
"feature test {"
" sub [a-z] by [A.sc-Z.sc];"
" sub e by e.fina;"
- "} test;")
+ "} test;",
+ )
def test_singlePos_redefinition(self):
self.assertRaisesRegex(
FeatureLibError,
- "Already defined different position for glyph \"A\"",
- self.build, "feature test { pos A 123; pos A 456; } test;")
+ 'Already defined different position for glyph "A"',
+ self.build,
+ "feature test { pos A 123; pos A 456; } test;",
+ )
def test_feature_outside_aalt(self):
self.assertRaisesRegex(
FeatureLibError,
'Feature references are only allowed inside "feature aalt"',
- self.build, "feature test { feature test; } test;")
+ self.build,
+ "feature test { feature test; } test;",
+ )
def test_feature_undefinedReference(self):
self.assertRaisesRegex(
- FeatureLibError, 'Feature none has not been defined',
- self.build, "feature aalt { feature none; } aalt;")
+ FeatureLibError,
+ "Feature none has not been defined",
+ self.build,
+ "feature aalt { feature none; } aalt;",
+ )
def test_GlyphClassDef_conflictingClasses(self):
self.assertRaisesRegex(
- FeatureLibError, "Glyph X was assigned to a different class",
+ FeatureLibError,
+ "Glyph X was assigned to a different class",
self.build,
"table GDEF {"
" GlyphClassDef [a b], [X], , ;"
" GlyphClassDef [a b X], , , ;"
- "} GDEF;")
+ "} GDEF;",
+ )
def test_languagesystem(self):
builder = Builder(makeTTFont(), (None, None))
- builder.add_language_system(None, 'latn', 'FRA')
- builder.add_language_system(None, 'cyrl', 'RUS')
- builder.start_feature(location=None, name='test')
- self.assertEqual(builder.language_systems,
- {('latn', 'FRA'), ('cyrl', 'RUS')})
+ builder.add_language_system(None, "latn", "FRA")
+ builder.add_language_system(None, "cyrl", "RUS")
+ builder.start_feature(location=None, name="test")
+ self.assertEqual(builder.language_systems, {("latn", "FRA"), ("cyrl", "RUS")})
def test_languagesystem_duplicate(self):
self.assertRaisesRegex(
FeatureLibError,
'"languagesystem cyrl RUS" has already been specified',
- self.build, "languagesystem cyrl RUS; languagesystem cyrl RUS;")
+ self.build,
+ "languagesystem cyrl RUS; languagesystem cyrl RUS;",
+ )
def test_languagesystem_none_specified(self):
builder = Builder(makeTTFont(), (None, None))
- builder.start_feature(location=None, name='test')
- self.assertEqual(builder.language_systems, {('DFLT', 'dflt')})
+ builder.start_feature(location=None, name="test")
+ self.assertEqual(builder.language_systems, {("DFLT", "dflt")})
def test_languagesystem_DFLT_dflt_not_first(self):
self.assertRaisesRegex(
FeatureLibError,
- "If \"languagesystem DFLT dflt\" is present, "
+ 'If "languagesystem DFLT dflt" is present, '
"it must be the first of the languagesystem statements",
- self.build, "languagesystem latn TRK; languagesystem DFLT dflt;")
+ self.build,
+ "languagesystem latn TRK; languagesystem DFLT dflt;",
+ )
def test_languagesystem_DFLT_not_preceding(self):
self.assertRaisesRegex(
FeatureLibError,
- "languagesystems using the \"DFLT\" script tag must "
+ 'languagesystems using the "DFLT" script tag must '
"precede all other languagesystems",
self.build,
"languagesystem DFLT dflt; "
"languagesystem latn dflt; "
- "languagesystem DFLT fooo; "
+ "languagesystem DFLT fooo; ",
)
def test_script(self):
builder = Builder(makeTTFont(), (None, None))
- builder.start_feature(location=None, name='test')
- builder.set_script(location=None, script='cyrl')
- self.assertEqual(builder.language_systems, {('cyrl', 'dflt')})
+ builder.start_feature(location=None, name="test")
+ builder.set_script(location=None, script="cyrl")
+ self.assertEqual(builder.language_systems, {("cyrl", "dflt")})
def test_script_in_aalt_feature(self):
self.assertRaisesRegex(
FeatureLibError,
- "Script statements are not allowed within \"feature aalt\"",
- self.build, "feature aalt { script latn; } aalt;")
+ 'Script statements are not allowed within "feature aalt"',
+ self.build,
+ "feature aalt { script latn; } aalt;",
+ )
def test_script_in_size_feature(self):
self.assertRaisesRegex(
FeatureLibError,
- "Script statements are not allowed within \"feature size\"",
- self.build, "feature size { script latn; } size;")
+ 'Script statements are not allowed within "feature size"',
+ self.build,
+ "feature size { script latn; } size;",
+ )
def test_script_in_standalone_lookup(self):
self.assertRaisesRegex(
FeatureLibError,
"Script statements are not allowed within standalone lookup blocks",
- self.build, "lookup test { script latn; } test;")
+ self.build,
+ "lookup test { script latn; } test;",
+ )
def test_language(self):
builder = Builder(makeTTFont(), (None, None))
- builder.add_language_system(None, 'latn', 'FRA ')
- builder.start_feature(location=None, name='test')
- builder.set_script(location=None, script='cyrl')
- builder.set_language(location=None, language='RUS ',
- include_default=False, required=False)
- self.assertEqual(builder.language_systems, {('cyrl', 'RUS ')})
- builder.set_language(location=None, language='BGR ',
- include_default=True, required=False)
- self.assertEqual(builder.language_systems,
- {('cyrl', 'BGR ')})
- builder.start_feature(location=None, name='test2')
- self.assertEqual(builder.language_systems, {('latn', 'FRA ')})
+ builder.add_language_system(None, "latn", "FRA ")
+ builder.start_feature(location=None, name="test")
+ builder.set_script(location=None, script="cyrl")
+ builder.set_language(
+ location=None, language="RUS ", include_default=False, required=False
+ )
+ self.assertEqual(builder.language_systems, {("cyrl", "RUS ")})
+ builder.set_language(
+ location=None, language="BGR ", include_default=True, required=False
+ )
+ self.assertEqual(builder.language_systems, {("cyrl", "BGR ")})
+ builder.start_feature(location=None, name="test2")
+ self.assertEqual(builder.language_systems, {("latn", "FRA ")})
def test_language_in_aalt_feature(self):
self.assertRaisesRegex(
FeatureLibError,
- "Language statements are not allowed within \"feature aalt\"",
- self.build, "feature aalt { language FRA; } aalt;")
+ 'Language statements are not allowed within "feature aalt"',
+ self.build,
+ "feature aalt { language FRA; } aalt;",
+ )
def test_language_in_size_feature(self):
self.assertRaisesRegex(
FeatureLibError,
- "Language statements are not allowed within \"feature size\"",
- self.build, "feature size { language FRA; } size;")
+ 'Language statements are not allowed within "feature size"',
+ self.build,
+ "feature size { language FRA; } size;",
+ )
def test_language_in_standalone_lookup(self):
self.assertRaisesRegex(
FeatureLibError,
"Language statements are not allowed within standalone lookup blocks",
- self.build, "lookup test { language FRA; } test;")
+ self.build,
+ "lookup test { language FRA; } test;",
+ )
def test_language_required_duplicate(self):
self.assertRaisesRegex(
@@ -409,13 +474,16 @@ class BuilderTest(unittest.TestCase):
" script latn;"
" language FRA required;"
" substitute [a-z] by [A.sc-Z.sc];"
- "} test;")
+ "} test;",
+ )
def test_lookup_already_defined(self):
self.assertRaisesRegex(
FeatureLibError,
- "Lookup \"foo\" has already been defined",
- self.build, "lookup foo {} foo; lookup foo {} foo;")
+ 'Lookup "foo" has already been defined',
+ self.build,
+ "lookup foo {} foo; lookup foo {} foo;",
+ )
def test_lookup_multiple_flags(self):
self.assertRaisesRegex(
@@ -428,7 +496,8 @@ class BuilderTest(unittest.TestCase):
" sub f i by f_i;"
" lookupflag 2;"
" sub f f i by f_f_i;"
- "} foo;")
+ "} foo;",
+ )
def test_lookup_multiple_types(self):
self.assertRaisesRegex(
@@ -439,13 +508,16 @@ class BuilderTest(unittest.TestCase):
"lookup foo {"
" sub f f i by f_f_i;"
" sub A from [A.alt1 A.alt2];"
- "} foo;")
+ "} foo;",
+ )
def test_lookup_inside_feature_aalt(self):
self.assertRaisesRegex(
FeatureLibError,
"Lookup blocks cannot be placed inside 'aalt' features",
- self.build, "feature aalt {lookup L {} L;} aalt;")
+ self.build,
+ "feature aalt {lookup L {} L;} aalt;",
+ )
def test_chain_subst_refrences_GPOS_looup(self):
self.assertRaisesRegex(
@@ -455,7 +527,7 @@ class BuilderTest(unittest.TestCase):
"lookup dummy { pos a 50; } dummy;"
"feature test {"
" sub a' lookup dummy b;"
- "} test;"
+ "} test;",
)
def test_chain_pos_refrences_GSUB_looup(self):
@@ -466,203 +538,215 @@ class BuilderTest(unittest.TestCase):
"lookup dummy { sub a by A; } dummy;"
"feature test {"
" pos a' lookup dummy b;"
- "} test;"
+ "} test;",
)
def test_STAT_elidedfallbackname_already_defined(self):
self.assertRaisesRegex(
FeatureLibError,
- 'ElidedFallbackName is already set.',
+ "ElidedFallbackName is already set.",
self.build,
- 'table name {'
+ "table name {"
' nameid 256 "Roman"; '
- '} name;'
- 'table STAT {'
+ "} name;"
+ "table STAT {"
' ElidedFallbackName { name "Roman"; };'
- ' ElidedFallbackNameID 256;'
- '} STAT;')
+ " ElidedFallbackNameID 256;"
+ "} STAT;",
+ )
def test_STAT_elidedfallbackname_set_twice(self):
self.assertRaisesRegex(
FeatureLibError,
- 'ElidedFallbackName is already set.',
+ "ElidedFallbackName is already set.",
self.build,
- 'table name {'
+ "table name {"
' nameid 256 "Roman"; '
- '} name;'
- 'table STAT {'
+ "} name;"
+ "table STAT {"
' ElidedFallbackName { name "Roman"; };'
' ElidedFallbackName { name "Italic"; };'
- '} STAT;')
+ "} STAT;",
+ )
def test_STAT_elidedfallbacknameID_already_defined(self):
self.assertRaisesRegex(
FeatureLibError,
- 'ElidedFallbackNameID is already set.',
+ "ElidedFallbackNameID is already set.",
self.build,
- 'table name {'
+ "table name {"
' nameid 256 "Roman"; '
- '} name;'
- 'table STAT {'
- ' ElidedFallbackNameID 256;'
+ "} name;"
+ "table STAT {"
+ " ElidedFallbackNameID 256;"
' ElidedFallbackName { name "Roman"; };'
- '} STAT;')
+ "} STAT;",
+ )
def test_STAT_elidedfallbacknameID_not_in_name_table(self):
self.assertRaisesRegex(
FeatureLibError,
- 'ElidedFallbackNameID 256 points to a nameID that does not '
+ "ElidedFallbackNameID 256 points to a nameID that does not "
'exist in the "name" table',
self.build,
- 'table name {'
+ "table name {"
' nameid 257 "Roman"; '
- '} name;'
- 'table STAT {'
- ' ElidedFallbackNameID 256;'
+ "} name;"
+ "table STAT {"
+ " ElidedFallbackNameID 256;"
' DesignAxis opsz 1 { name "Optical Size"; };'
- '} STAT;')
+ "} STAT;",
+ )
def test_STAT_design_axis_name(self):
self.assertRaisesRegex(
FeatureLibError,
'Expected "name"',
self.build,
- 'table name {'
+ "table name {"
' nameid 256 "Roman"; '
- '} name;'
- 'table STAT {'
+ "} name;"
+ "table STAT {"
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { badtag "Optical Size"; };'
- '} STAT;')
+ "} STAT;",
+ )
def test_STAT_duplicate_design_axis_name(self):
self.assertRaisesRegex(
FeatureLibError,
'DesignAxis already defined for tag "opsz".',
self.build,
- 'table name {'
+ "table name {"
' nameid 256 "Roman"; '
- '} name;'
- 'table STAT {'
+ "} name;"
+ "table STAT {"
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { name "Optical Size"; };'
' DesignAxis opsz 1 { name "Optical Size"; };'
- '} STAT;')
+ "} STAT;",
+ )
def test_STAT_design_axis_duplicate_order(self):
self.assertRaisesRegex(
FeatureLibError,
"DesignAxis already defined for axis number 0.",
self.build,
- 'table name {'
+ "table name {"
' nameid 256 "Roman"; '
- '} name;'
- 'table STAT {'
+ "} name;"
+ "table STAT {"
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { name "Optical Size"; };'
' DesignAxis wdth 0 { name "Width"; };'
- ' AxisValue {'
- ' location opsz 8;'
- ' location wdth 400;'
+ " AxisValue {"
+ " location opsz 8;"
+ " location wdth 400;"
' name "Caption";'
- ' };'
- '} STAT;')
+ " };"
+ "} STAT;",
+ )
def test_STAT_undefined_tag(self):
self.assertRaisesRegex(
FeatureLibError,
- 'DesignAxis not defined for wdth.',
+ "DesignAxis not defined for wdth.",
self.build,
- 'table name {'
+ "table name {"
' nameid 256 "Roman"; '
- '} name;'
- 'table STAT {'
+ "} name;"
+ "table STAT {"
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { name "Optical Size"; };'
- ' AxisValue { '
- ' location wdth 125; '
+ " AxisValue { "
+ " location wdth 125; "
' name "Wide"; '
- ' };'
- '} STAT;')
+ " };"
+ "} STAT;",
+ )
def test_STAT_axis_value_format4(self):
self.assertRaisesRegex(
FeatureLibError,
- 'Axis tag wdth already defined.',
+ "Axis tag wdth already defined.",
self.build,
- 'table name {'
+ "table name {"
' nameid 256 "Roman"; '
- '} name;'
- 'table STAT {'
+ "} name;"
+ "table STAT {"
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { name "Optical Size"; };'
' DesignAxis wdth 1 { name "Width"; };'
' DesignAxis wght 2 { name "Weight"; };'
- ' AxisValue { '
- ' location opsz 8; '
- ' location wdth 125; '
- ' location wdth 125; '
- ' location wght 500; '
+ " AxisValue { "
+ " location opsz 8; "
+ " location wdth 125; "
+ " location wdth 125; "
+ " location wght 500; "
' name "Caption Medium Wide"; '
- ' };'
- '} STAT;')
+ " };"
+ "} STAT;",
+ )
def test_STAT_duplicate_axis_value_record(self):
# Test for Duplicate AxisValueRecords even when the definition order
# is different.
self.assertRaisesRegex(
FeatureLibError,
- 'An AxisValueRecord with these values is already defined.',
+ "An AxisValueRecord with these values is already defined.",
self.build,
- 'table name {'
+ "table name {"
' nameid 256 "Roman"; '
- '} name;'
- 'table STAT {'
+ "} name;"
+ "table STAT {"
' ElidedFallbackName { name "Roman"; };'
' DesignAxis opsz 0 { name "Optical Size"; };'
' DesignAxis wdth 1 { name "Width"; };'
- ' AxisValue {'
- ' location opsz 8;'
- ' location wdth 400;'
+ " AxisValue {"
+ " location opsz 8;"
+ " location wdth 400;"
' name "Caption";'
- ' };'
- ' AxisValue {'
- ' location wdth 400;'
- ' location opsz 8;'
+ " };"
+ " AxisValue {"
+ " location wdth 400;"
+ " location opsz 8;"
' name "Caption";'
- ' };'
- '} STAT;')
+ " };"
+ "} STAT;",
+ )
def test_STAT_axis_value_missing_location(self):
self.assertRaisesRegex(
FeatureLibError,
'Expected "Axis location"',
self.build,
- 'table name {'
+ "table name {"
' nameid 256 "Roman"; '
- '} name;'
- 'table STAT {'
+ "} name;"
+ "table STAT {"
' ElidedFallbackName { name "Roman"; '
- '};'
+ "};"
' DesignAxis opsz 0 { name "Optical Size"; };'
- ' AxisValue { '
+ " AxisValue { "
' name "Wide"; '
- ' };'
- '} STAT;')
+ " };"
+ "} STAT;",
+ )
def test_STAT_invalid_location_tag(self):
self.assertRaisesRegex(
FeatureLibError,
- 'Tags cannot be longer than 4 characters',
+ "Tags cannot be longer than 4 characters",
self.build,
- 'table name {'
+ "table name {"
' nameid 256 "Roman"; '
- '} name;'
- 'table STAT {'
+ "} name;"
+ "table STAT {"
' ElidedFallbackName { name "Roman"; '
' name 3 1 0x0411 "ローマン"; }; '
' DesignAxis width 0 { name "Width"; };'
- '} STAT;')
+ "} STAT;",
+ )
def test_extensions(self):
class ast_BaseClass(ast.MarkClass):
@@ -680,7 +764,9 @@ class BuilderTest(unittest.TestCase):
for bcd in self.base.markClass.definitions:
if res != "":
res += "\n{}".format(indent)
- res += "pos base {} {}".format(bcd.glyphs.asFea(), bcd.anchor.asFea())
+ res += "pos base {} {}".format(
+ bcd.glyphs.asFea(), bcd.anchor.asFea()
+ )
for m in self.marks:
res += " mark @{}".format(m.name)
res += ";"
@@ -693,6 +779,7 @@ class BuilderTest(unittest.TestCase):
class testAst(object):
MarkBasePosStatement = ast_MarkBasePosStatement
+
def __getattr__(self, name):
return getattr(ast, name)
@@ -703,8 +790,9 @@ class BuilderTest(unittest.TestCase):
if enumerated:
raise FeatureLibError(
'"enumerate" is not allowed with '
- 'mark-to-base attachment positioning',
- location)
+ "mark-to-base attachment positioning",
+ location,
+ )
base = self.parse_glyphclass_(accept_glyphname=True)
if self.next_token_ == "<":
marks = self.parse_anchor_marks_()
@@ -715,11 +803,10 @@ class BuilderTest(unittest.TestCase):
m = self.expect_markClass_reference_()
marks.append(m)
self.expect_symbol_(";")
- return self.ast.MarkBasePosStatement(base, marks,
- location=location)
+ return self.ast.MarkBasePosStatement(base, marks, location=location)
def parseBaseClass(self):
- if not hasattr(self.doc_, 'baseClasses'):
+ if not hasattr(self.doc_, "baseClasses"):
self.doc_.baseClasses = {}
location = self.cur_token_location_
glyphs = self.parse_glyphclass_(accept_glyphname=True)
@@ -731,37 +818,39 @@ class BuilderTest(unittest.TestCase):
baseClass = ast_BaseClass(name)
self.doc_.baseClasses[name] = baseClass
self.glyphclasses_.define(name, baseClass)
- bcdef = ast_BaseClassDefinition(baseClass, anchor, glyphs,
- location=location)
+ bcdef = ast_BaseClassDefinition(
+ baseClass, anchor, glyphs, location=location
+ )
baseClass.addDefinition(bcdef)
return bcdef
- extensions = {
- 'baseClass' : lambda s : s.parseBaseClass()
- }
+ extensions = {"baseClass": lambda s: s.parseBaseClass()}
ast = testAst()
self.check_fea2fea_file(
- "baseClass.feax", base="baseClass.fea", parser=testParser)
+ "baseClass.feax", base="baseClass.fea", parser=testParser
+ )
def test_markClass_same_glyph_redefined(self):
self.assertRaisesRegex(
FeatureLibError,
"Glyph acute already defined",
self.build,
- "markClass [acute] <anchor 350 0> @TOP_MARKS;"*2)
+ "markClass [acute] <anchor 350 0> @TOP_MARKS;" * 2,
+ )
def test_markClass_same_glyph_multiple_classes(self):
self.assertRaisesRegex(
FeatureLibError,
- 'Glyph uni0327 cannot be in both @ogonek and @cedilla',
+ "Glyph uni0327 cannot be in both @ogonek and @cedilla",
self.build,
"feature mark {"
" markClass [uni0327 uni0328] <anchor 0 0> @ogonek;"
" pos base [a] <anchor 399 0> mark @ogonek;"
" markClass [uni0327] <anchor 0 0> @cedilla;"
" pos base [a] <anchor 244 0> mark @cedilla;"
- "} mark;")
+ "} mark;",
+ )
def test_build_specific_tables(self):
features = "feature liga {sub f i by f_i;} liga;"
@@ -783,7 +872,7 @@ class BuilderTest(unittest.TestCase):
def test_unsupported_subtable_break(self):
logger = logging.getLogger("fontTools.otlLib.builder")
- with CapturingLogHandler(logger, level='WARNING') as captor:
+ with CapturingLogHandler(logger, level="WARNING") as captor:
self.build(
"feature test {"
" pos a 10;"
@@ -814,10 +903,8 @@ class BuilderTest(unittest.TestCase):
FeatureLibError,
"Already defined different position for glyph",
self.build,
- "lookup foo {"
- " pos A -45; "
- " pos A 45; "
- "} foo;")
+ "lookup foo {" " pos A -45; " " pos A 45; " "} foo;",
+ )
def test_pairPos_enumRuleOverridenBySinglePair_DEBUG(self):
logger = logging.getLogger("fontTools.otlLib.builder")
@@ -826,8 +913,56 @@ class BuilderTest(unittest.TestCase):
"feature test {"
" enum pos A [V Y] -80;"
" pos A V -75;"
- "} test;")
- captor.assertRegex('Already defined position for pair A V at')
+ "} test;"
+ )
+ captor.assertRegex("Already defined position for pair A V at")
+
+ def test_ignore_empty_lookup_block(self):
+ # https://github.com/fonttools/fonttools/pull/2277
+ font = self.build(
+ "lookup EMPTY { ; } EMPTY;" "feature ss01 { lookup EMPTY; } ss01;"
+ )
+ assert "GPOS" not in font
+ assert "GSUB" not in font
+
+ def test_disable_empty_classes(self):
+ for test in [
+ "sub a by c []",
+ "sub f f [] by f",
+ "ignore sub a []'",
+ "ignore sub [] a'",
+ "sub a []' by b",
+ "sub [] a' by b",
+ "rsub [] by a",
+ "pos [] 120",
+ "pos a [] 120",
+ "enum pos a [] 120",
+ "pos cursive [] <anchor NULL> <anchor NULL>",
+ "pos base [] <anchor NULL> mark @TOPMARKS",
+ "pos ligature [] <anchor NULL> mark @TOPMARKS",
+ "pos mark [] <anchor NULL> mark @TOPMARKS",
+ "ignore pos a []'",
+ "ignore pos [] a'",
+ ]:
+ self.assertRaisesRegex(
+ FeatureLibError,
+ "Empty ",
+ self.build,
+ f"markClass a <anchor 150 -10> @TOPMARKS; lookup foo {{ {test}; }} foo;",
+ )
+ self.assertRaisesRegex(
+ FeatureLibError,
+ "Empty glyph class in mark class definition",
+ self.build,
+ "markClass [] <anchor 150 -10> @TOPMARKS;"
+ )
+ self.assertRaisesRegex(
+ FeatureLibError,
+ 'Expected a glyph class with 1 elements after "by", but found a glyph class with 0 elements',
+ self.build,
+ "feature test { sub a by []; test};"
+ )
+
def generate_feature_file_test(name):
@@ -835,8 +970,7 @@ def generate_feature_file_test(name):
for name in BuilderTest.TEST_FEATURE_FILES:
- setattr(BuilderTest, "test_FeatureFile_%s" % name,
- generate_feature_file_test(name))
+ setattr(BuilderTest, "test_FeatureFile_%s" % name, generate_feature_file_test(name))
def generate_fea2fea_file_test(name):
@@ -844,8 +978,11 @@ def generate_fea2fea_file_test(name):
for name in BuilderTest.TEST_FEATURE_FILES:
- setattr(BuilderTest, "test_Fea2feaFile_{}".format(name),
- generate_fea2fea_file_test(name))
+ setattr(
+ BuilderTest,
+ "test_Fea2feaFile_{}".format(name),
+ generate_fea2fea_file_test(name),
+ )
if __name__ == "__main__":
diff --git a/Tests/feaLib/data/PairPosSubtable.fea b/Tests/feaLib/data/PairPosSubtable.fea
index cb78801c..1fcc1eba 100644
--- a/Tests/feaLib/data/PairPosSubtable.fea
+++ b/Tests/feaLib/data/PairPosSubtable.fea
@@ -4,7 +4,6 @@ languagesystem latn dflt;
@group1 = [b o];
@group2 = [c d];
@group3 = [v w];
-@group4 = [];
lookup kernlookup {
pos A V -34;
@@ -13,9 +12,6 @@ lookup kernlookup {
subtable;
pos @group1 @group3 -10;
pos @group3 @group2 -20;
- subtable;
- pos @group4 @group1 -10;
- pos @group4 @group4 -10;
} kernlookup;
feature kern {
diff --git a/Tests/feaLib/data/bug2276.fea b/Tests/feaLib/data/bug2276.fea
new file mode 100644
index 00000000..96f98859
--- /dev/null
+++ b/Tests/feaLib/data/bug2276.fea
@@ -0,0 +1,11 @@
+# https://github.com/fonttools/fonttools/issues/2276
+lookup EMPTY {
+ # pass
+} EMPTY;
+feature ss01 {
+ sub a by a.alt1;
+ lookup EMPTY;
+} ss01;
+feature aalt {
+ feature ss01;
+} aalt;
diff --git a/Tests/feaLib/data/bug2276.ttx b/Tests/feaLib/data/bug2276.ttx
new file mode 100644
index 00000000..57902daf
--- /dev/null
+++ b/Tests/feaLib/data/bug2276.ttx
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.22">
+
+ <GSUB>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=1 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=2 -->
+ <FeatureIndex index="0" value="0"/>
+ <FeatureIndex index="1" value="1"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=2 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="aalt"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </FeatureRecord>
+ <FeatureRecord index="1">
+ <FeatureTag value="ss01"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="1"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=2 -->
+ <Lookup index="0">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0">
+ <Substitution in="a" out="a.alt1"/>
+ </SingleSubst>
+ </Lookup>
+ <Lookup index="1">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0">
+ <Substitution in="a" out="a.alt1"/>
+ </SingleSubst>
+ </Lookup>
+ </LookupList>
+ </GSUB>
+
+</ttFont>
diff --git a/Tests/feaLib/data/delete_glyph.fea b/Tests/feaLib/data/delete_glyph.fea
index 36e0f0f9..ab4b93f1 100644
--- a/Tests/feaLib/data/delete_glyph.fea
+++ b/Tests/feaLib/data/delete_glyph.fea
@@ -1,3 +1,7 @@
feature test {
sub a by NULL;
} test;
+
+feature test {
+ sub [a b c] by NULL;
+} test;
diff --git a/Tests/feaLib/data/delete_glyph.ttx b/Tests/feaLib/data/delete_glyph.ttx
index 777f6e36..b28259fb 100644
--- a/Tests/feaLib/data/delete_glyph.ttx
+++ b/Tests/feaLib/data/delete_glyph.ttx
@@ -22,13 +22,14 @@
<FeatureRecord index="0">
<FeatureTag value="test"/>
<Feature>
- <!-- LookupCount=1 -->
+ <!-- LookupCount=2 -->
<LookupListIndex index="0" value="0"/>
+ <LookupListIndex index="1" value="1"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
- <!-- LookupCount=1 -->
+ <!-- LookupCount=2 -->
<Lookup index="0">
<LookupType value="2"/>
<LookupFlag value="0"/>
@@ -37,6 +38,16 @@
<Substitution in="a" out=""/>
</MultipleSubst>
</Lookup>
+ <Lookup index="1">
+ <LookupType value="2"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <MultipleSubst index="0">
+ <Substitution in="a" out=""/>
+ <Substitution in="b" out=""/>
+ <Substitution in="c" out=""/>
+ </MultipleSubst>
+ </Lookup>
</LookupList>
</GSUB>
diff --git a/Tests/feaLib/data/variable_conditionset.fea b/Tests/feaLib/data/variable_conditionset.fea
new file mode 100644
index 00000000..7009c62b
--- /dev/null
+++ b/Tests/feaLib/data/variable_conditionset.fea
@@ -0,0 +1,13 @@
+languagesystem DFLT dflt;
+
+lookup symbols_heavy {
+ sub a by b;
+} symbols_heavy;
+
+conditionset heavy {
+ wght 700 900;
+} heavy;
+
+variation rvrn heavy {
+ lookup symbols_heavy;
+} rvrn;
diff --git a/Tests/feaLib/data/variable_conditionset.ttx b/Tests/feaLib/data/variable_conditionset.ttx
new file mode 100644
index 00000000..18b156fa
--- /dev/null
+++ b/Tests/feaLib/data/variable_conditionset.ttx
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont>
+
+ <GSUB>
+ <Version value="0x00010001"/>
+ <ScriptList>
+ <!-- ScriptCount=1 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="0"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=1 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="rvrn"/>
+ <Feature>
+ <!-- LookupCount=0 -->
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=1 -->
+ <Lookup index="0">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0">
+ <Substitution in="a" out="b"/>
+ </SingleSubst>
+ </Lookup>
+ </LookupList>
+ <FeatureVariations>
+ <Version value="0x00010000"/>
+ <!-- FeatureVariationCount=1 -->
+ <FeatureVariationRecord index="0">
+ <ConditionSet>
+ <!-- ConditionCount=1 -->
+ <ConditionTable index="0" Format="1">
+ <AxisIndex value="0"/>
+ <FilterRangeMinValue value="0.625"/>
+ <FilterRangeMaxValue value="0.875"/>
+ </ConditionTable>
+ </ConditionSet>
+ <FeatureTableSubstitution>
+ <Version value="0x00010000"/>
+ <!-- SubstitutionCount=1 -->
+ <SubstitutionRecord index="0">
+ <FeatureIndex value="0"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </SubstitutionRecord>
+ </FeatureTableSubstitution>
+ </FeatureVariationRecord>
+ </FeatureVariations>
+ </GSUB>
+
+</ttFont>
diff --git a/Tests/feaLib/data/variable_scalar_anchor.fea b/Tests/feaLib/data/variable_scalar_anchor.fea
new file mode 100644
index 00000000..c4787986
--- /dev/null
+++ b/Tests/feaLib/data/variable_scalar_anchor.fea
@@ -0,0 +1,4 @@
+languagesystem DFLT dflt;
+feature kern {
+ pos cursive one <anchor 0 (wght=200:12 wght=900:22 wdth=150,wght=900:42)> <anchor NULL>;
+} kern;
diff --git a/Tests/feaLib/data/variable_scalar_anchor.ttx b/Tests/feaLib/data/variable_scalar_anchor.ttx
new file mode 100644
index 00000000..6bb55691
--- /dev/null
+++ b/Tests/feaLib/data/variable_scalar_anchor.ttx
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont>
+
+ <GDEF>
+ <Version value="0x00010003"/>
+ <VarStore Format="1">
+ <Format value="1"/>
+ <VarRegionList>
+ <!-- RegionAxisCount=2 -->
+ <!-- RegionCount=2 -->
+ <Region index="0">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.875"/>
+ <EndCoord value="0.875"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="1">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.875"/>
+ <EndCoord value="0.875"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.5"/>
+ <EndCoord value="0.5"/>
+ </VarRegionAxis>
+ </Region>
+ </VarRegionList>
+ <!-- VarDataCount=1 -->
+ <VarData index="0">
+ <!-- ItemCount=1 -->
+ <NumShorts value="0"/>
+ <!-- VarRegionCount=2 -->
+ <VarRegionIndex index="0" value="0"/>
+ <VarRegionIndex index="1" value="1"/>
+ <Item index="0" value="[10, 20]"/>
+ </VarData>
+ </VarStore>
+ </GDEF>
+
+ <GPOS>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=1 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="0"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=1 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="kern"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=1 -->
+ <Lookup index="0">
+ <LookupType value="3"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <CursivePos index="0" Format="1">
+ <Coverage>
+ <Glyph value="one"/>
+ </Coverage>
+ <!-- EntryExitCount=1 -->
+ <EntryExitRecord index="0">
+ <EntryAnchor Format="3">
+ <XCoordinate value="0"/>
+ <YCoordinate value="12"/>
+ <YDeviceTable>
+ <StartSize value="0"/>
+ <EndSize value="0"/>
+ <DeltaFormat value="32768"/>
+ </YDeviceTable>
+ </EntryAnchor>
+ </EntryExitRecord>
+ </CursivePos>
+ </Lookup>
+ </LookupList>
+ </GPOS>
+
+</ttFont>
diff --git a/Tests/feaLib/data/variable_scalar_valuerecord.fea b/Tests/feaLib/data/variable_scalar_valuerecord.fea
new file mode 100644
index 00000000..bf9a26b7
--- /dev/null
+++ b/Tests/feaLib/data/variable_scalar_valuerecord.fea
@@ -0,0 +1,5 @@
+languagesystem DFLT dflt;
+feature kern {
+ pos one 1;
+ pos two <0 (wght=200:12 wght=900:22 wdth=150,wght=900:42) 0 0>;
+} kern;
diff --git a/Tests/feaLib/data/variable_scalar_valuerecord.ttx b/Tests/feaLib/data/variable_scalar_valuerecord.ttx
new file mode 100644
index 00000000..338b7221
--- /dev/null
+++ b/Tests/feaLib/data/variable_scalar_valuerecord.ttx
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont>
+
+ <GDEF>
+ <Version value="0x00010003"/>
+ <VarStore Format="1">
+ <Format value="1"/>
+ <VarRegionList>
+ <!-- RegionAxisCount=2 -->
+ <!-- RegionCount=2 -->
+ <Region index="0">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.875"/>
+ <EndCoord value="0.875"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="1">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.875"/>
+ <EndCoord value="0.875"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.5"/>
+ <EndCoord value="0.5"/>
+ </VarRegionAxis>
+ </Region>
+ </VarRegionList>
+ <!-- VarDataCount=1 -->
+ <VarData index="0">
+ <!-- ItemCount=1 -->
+ <NumShorts value="0"/>
+ <!-- VarRegionCount=2 -->
+ <VarRegionIndex index="0" value="0"/>
+ <VarRegionIndex index="1" value="1"/>
+ <Item index="0" value="[10, 20]"/>
+ </VarData>
+ </VarStore>
+ </GDEF>
+
+ <GPOS>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=1 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="0"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=1 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="kern"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=1 -->
+ <Lookup index="0">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=2 -->
+ <SinglePos index="0" Format="1">
+ <Coverage>
+ <Glyph value="one"/>
+ </Coverage>
+ <ValueFormat value="4"/>
+ <Value XAdvance="1"/>
+ </SinglePos>
+ <SinglePos index="1" Format="1">
+ <Coverage>
+ <Glyph value="two"/>
+ </Coverage>
+ <ValueFormat value="34"/>
+ <Value YPlacement="12">
+ <YPlaDevice>
+ <StartSize value="0"/>
+ <EndSize value="0"/>
+ <DeltaFormat value="32768"/>
+ </YPlaDevice>
+ </Value>
+ </SinglePos>
+ </Lookup>
+ </LookupList>
+ </GPOS>
+
+</ttFont>
diff --git a/Tests/feaLib/lexer_test.py b/Tests/feaLib/lexer_test.py
index 24dc5dba..3df67f70 100644
--- a/Tests/feaLib/lexer_test.py
+++ b/Tests/feaLib/lexer_test.py
@@ -1,6 +1,6 @@
-from fontTools.misc.py23 import tobytes
from fontTools.feaLib.error import FeatureLibError, IncludedFeaNotFound
from fontTools.feaLib.lexer import IncludingLexer, Lexer
+from fontTools.misc.textTools import tobytes
from io import StringIO
import os
import shutil
diff --git a/Tests/feaLib/parser_test.py b/Tests/feaLib/parser_test.py
index de2bc3ca..fd9dea70 100644
--- a/Tests/feaLib/parser_test.py
+++ b/Tests/feaLib/parser_test.py
@@ -14,8 +14,9 @@ def glyphstr(glyphs):
if len(x) == 1:
return list(x)[0]
else:
- return '[%s]' % ' '.join(sorted(list(x)))
- return ' '.join(f(g.glyphSet()) for g in glyphs)
+ return "[%s]" % " ".join(sorted(list(x)))
+
+ return " ".join(f(g.glyphSet()) for g in glyphs)
def mapping(s):
@@ -30,7 +31,9 @@ def mapping(s):
return dict(zip(b, c))
-GLYPHNAMES = ("""
+GLYPHNAMES = (
+ (
+ """
.notdef space A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
A.sc B.sc C.sc D.sc E.sc F.sc G.sc H.sc I.sc J.sc K.sc L.sc M.sc
N.sc O.sc P.sc Q.sc R.sc S.sc T.sc U.sc V.sc W.sc X.sc Y.sc Z.sc
@@ -48,7 +51,10 @@ GLYPHNAMES = ("""
cid00111 cid00222
comma endash emdash figuredash damma hamza
c_d d.alt n.end s.end f_f
-""").split() + ["foo.%d" % i for i in range(1, 200)]
+"""
+ ).split()
+ + ["foo.%d" % i for i in range(1, 200)]
+)
class ParserTest(unittest.TestCase):
@@ -60,7 +66,7 @@ class ParserTest(unittest.TestCase):
self.assertRaisesRegex = self.assertRaisesRegexp
def test_glyphMap_deprecated(self):
- glyphMap = {'a': 0, 'b': 1, 'c': 2}
+ glyphMap = {"a": 0, "b": 1, "c": 2}
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
parser = Parser(StringIO(), glyphMap=glyphMap)
@@ -68,22 +74,28 @@ class ParserTest(unittest.TestCase):
self.assertEqual(len(w), 1)
self.assertEqual(w[-1].category, UserWarning)
self.assertIn("deprecated", str(w[-1].message))
- self.assertEqual(parser.glyphNames_, {'a', 'b', 'c'})
+ self.assertEqual(parser.glyphNames_, {"a", "b", "c"})
self.assertRaisesRegex(
- TypeError, "mutually exclusive",
- Parser, StringIO(), ("a",), glyphMap={"a": 0})
+ TypeError,
+ "mutually exclusive",
+ Parser,
+ StringIO(),
+ ("a",),
+ glyphMap={"a": 0},
+ )
self.assertRaisesRegex(
- TypeError, "unsupported keyword argument",
- Parser, StringIO(), foo="bar")
+ TypeError, "unsupported keyword argument", Parser, StringIO(), foo="bar"
+ )
def test_comments(self):
doc = self.parse(
""" # Initial
feature test {
sub A by B; # simple
- } test;""")
+ } test;"""
+ )
c1 = doc.statements[0]
c2 = doc.statements[1].statements[1]
self.assertEqual(type(c1), ast.Comment)
@@ -94,9 +106,11 @@ class ParserTest(unittest.TestCase):
self.assertEqual(doc.statements[1].name, "test")
def test_only_comments(self):
- doc = self.parse("""\
+ doc = self.parse(
+ """\
# Initial
- """)
+ """
+ )
c1 = doc.statements[0]
self.assertEqual(type(c1), ast.Comment)
self.assertEqual(c1.text, "# Initial")
@@ -106,7 +120,8 @@ class ParserTest(unittest.TestCase):
doc = self.parse(
"feature test {"
" pos cursive A <anchor 120 -20> <anchor NULL>;"
- "} test;")
+ "} test;"
+ )
anchor = doc.statements[0].statements[0].entryAnchor
self.assertEqual(type(anchor), ast.Anchor)
self.assertEqual(anchor.x, 120)
@@ -119,7 +134,8 @@ class ParserTest(unittest.TestCase):
doc = self.parse(
"feature test {"
" pos cursive A <anchor 120 -20 contourpoint 5> <anchor NULL>;"
- "} test;")
+ "} test;"
+ )
anchor = doc.statements[0].statements[0].entryAnchor
self.assertEqual(type(anchor), ast.Anchor)
self.assertEqual(anchor.x, 120)
@@ -134,7 +150,8 @@ class ParserTest(unittest.TestCase):
" pos cursive A "
" <anchor 120 -20 <device 11 111, 12 112> <device NULL>>"
" <anchor NULL>;"
- "} test;")
+ "} test;"
+ )
anchor = doc.statements[0].statements[0].entryAnchor
self.assertEqual(type(anchor), ast.Anchor)
self.assertEqual(anchor.x, 120)
@@ -147,7 +164,8 @@ class ParserTest(unittest.TestCase):
doc = self.parse(
"feature test {"
" pos cursive A <anchor 120 -20> <anchor NULL>;"
- "} test;")
+ "} test;"
+ )
anchor = doc.statements[0].statements[0].exitAnchor
self.assertIsNone(anchor)
@@ -156,7 +174,8 @@ class ParserTest(unittest.TestCase):
"feature test {"
" anchorDef 120 -20 contourpoint 7 Foo;"
" pos cursive A <anchor Foo> <anchor NULL>;"
- "} test;")
+ "} test;"
+ )
anchor = doc.statements[0].statements[1].entryAnchor
self.assertEqual(type(anchor), ast.Anchor)
self.assertEqual(anchor.x, 120)
@@ -167,10 +186,25 @@ class ParserTest(unittest.TestCase):
def test_anchor_format_e_undefined(self):
self.assertRaisesRegex(
- FeatureLibError, 'Unknown anchor "UnknownName"', self.parse,
+ FeatureLibError,
+ 'Unknown anchor "UnknownName"',
+ self.parse,
"feature test {"
" position cursive A <anchor UnknownName> <anchor NULL>;"
- "} test;")
+ "} test;",
+ )
+
+ def test_anchor_variable_scalar(self):
+ doc = self.parse(
+ "feature test {"
+ " pos cursive A <anchor (wght=200:-100 wght=900:-150 wdth=150,wght=900:-120) -20> <anchor NULL>;"
+ "} test;"
+ )
+ anchor = doc.statements[0].statements[0].entryAnchor
+ self.assertEqual(
+ anchor.asFea(),
+ "<anchor (wght=200:-100 wght=900:-150 wdth=150,wght=900:-120) -20>",
+ )
def test_anchordef(self):
[foo] = self.parse("anchorDef 123 456 foo;").statements
@@ -203,8 +237,11 @@ class ParserTest(unittest.TestCase):
def test_anon_missingBrace(self):
self.assertRaisesRegex(
- FeatureLibError, "Expected '} TEST;' to terminate anonymous block",
- self.parse, "anon TEST { \n no end in sight")
+ FeatureLibError,
+ "Expected '} TEST;' to terminate anonymous block",
+ self.parse,
+ "anon TEST { \n no end in sight",
+ )
def test_attach(self):
doc = self.parse("table GDEF {Attach [a e] 2;} GDEF;")
@@ -222,8 +259,7 @@ class ParserTest(unittest.TestCase):
[liga] = self.parse("feature liga useExtension {} liga;").statements
self.assertEqual(liga.name, "liga")
self.assertTrue(liga.use_extension)
- self.assertEqual(liga.asFea(),
- "feature liga useExtension {\n \n} liga;\n")
+ self.assertEqual(liga.asFea(), "feature liga useExtension {\n \n} liga;\n")
def test_feature_comment(self):
[liga] = self.parse("feature liga { # Comment\n } liga;").statements
@@ -239,12 +275,16 @@ class ParserTest(unittest.TestCase):
def test_FeatureNames_bad(self):
self.assertRaisesRegex(
- FeatureLibError, 'Expected "name"',
- self.parse, "feature ss01 { featureNames { feature test; } ss01;")
+ FeatureLibError,
+ 'Expected "name"',
+ self.parse,
+ "feature ss01 { featureNames { feature test; } ss01;",
+ )
def test_FeatureNames_comment(self):
[feature] = self.parse(
- "feature ss01 { featureNames { # Comment\n }; } ss01;").statements
+ "feature ss01 { featureNames { # Comment\n }; } ss01;"
+ ).statements
[featureNames] = feature.statements
self.assertIsInstance(featureNames, ast.NestedBlock)
[comment] = featureNames.statements
@@ -253,7 +293,8 @@ class ParserTest(unittest.TestCase):
def test_FeatureNames_emptyStatements(self):
[feature] = self.parse(
- "feature ss01 { featureNames { ;;; }; } ss01;").statements
+ "feature ss01 { featureNames { ;;; }; } ss01;"
+ ).statements
[featureNames] = feature.statements
self.assertIsInstance(featureNames, ast.NestedBlock)
self.assertEqual(featureNames.statements, [])
@@ -266,8 +307,11 @@ class ParserTest(unittest.TestCase):
def test_FontRevision_negative(self):
self.assertRaisesRegex(
- FeatureLibError, "Font revision numbers must be positive",
- self.parse, "table head {FontRevision -17.2;} head;")
+ FeatureLibError,
+ "Font revision numbers must be positive",
+ self.parse,
+ "table head {FontRevision -17.2;} head;",
+ )
def test_strict_glyph_name_check(self):
self.parse("@bad = [a b ccc];", glyphNames=("a", "b", "ccc"))
@@ -282,14 +326,19 @@ class ParserTest(unittest.TestCase):
def test_glyphclass_glyphNameTooLong(self):
self.assertRaisesRegex(
- FeatureLibError, "must not be longer than 63 characters",
- self.parse, "@GlyphClass = [%s];" % ("G" * 64))
+ FeatureLibError,
+ "must not be longer than 63 characters",
+ self.parse,
+ "@GlyphClass = [%s];" % ("G" * 64),
+ )
def test_glyphclass_bad(self):
self.assertRaisesRegex(
FeatureLibError,
"Expected glyph name, glyph range, or glyph class reference",
- self.parse, "@bad = [a 123];")
+ self.parse,
+ "@bad = [a 123];",
+ )
def test_glyphclass_duplicate(self):
# makeotf accepts this, so we should too
@@ -312,9 +361,11 @@ class ParserTest(unittest.TestCase):
"markClass [acute grave] <anchor 500 800> @TOP_MARKS;"
"markClass cedilla <anchor 500 -100> @BOTTOM_MARKS;"
"@MARKS = [@TOP_MARKS @BOTTOM_MARKS ogonek];"
- "@ALL = @MARKS;")
- self.assertEqual(doc.statements[-1].glyphSet(),
- ("acute", "grave", "cedilla", "ogonek"))
+ "@ALL = @MARKS;"
+ )
+ self.assertEqual(
+ doc.statements[-1].glyphSet(), ("acute", "grave", "cedilla", "ogonek")
+ )
def test_glyphclass_range_cid(self):
[gc] = self.parse(r"@GlyphClass = [\999-\1001];").statements
@@ -325,7 +376,9 @@ class ParserTest(unittest.TestCase):
self.assertRaisesRegex(
FeatureLibError,
"Bad range: start should be less than limit",
- self.parse, r"@bad = [\998-\995];")
+ self.parse,
+ r"@bad = [\998-\995];",
+ )
def test_glyphclass_range_uppercase(self):
[gc] = self.parse("@swashes = [X.swash-Z.swash];").statements
@@ -355,7 +408,9 @@ class ParserTest(unittest.TestCase):
# https://github.com/fonttools/fonttools/issues/1768
glyphNames = ()
with CapturingLogHandler("fontTools.feaLib.parser", level="WARNING") as caplog:
- [gc] = self.parse("@class = [A-foo.sc B-foo.sc C D];", glyphNames).statements
+ [gc] = self.parse(
+ "@class = [A-foo.sc B-foo.sc C D];", glyphNames
+ ).statements
self.assertEqual(gc.glyphSet(), ("A-foo.sc", "B-foo.sc", "C", "D"))
self.assertEqual(len(caplog.records), 2)
caplog.assertRegex("Ambiguous glyph name that looks like a range:")
@@ -364,8 +419,7 @@ class ParserTest(unittest.TestCase):
# The OpenType Feature File Specification v1.20 makes it clear
# that if a dashed name could be interpreted either as a glyph name
# or as a range, then the semantics should be the single dashed name.
- glyphNames = (
- "A-foo.sc-C-foo.sc A-foo.sc B-foo.sc C-foo.sc".split())
+ glyphNames = "A-foo.sc-C-foo.sc A-foo.sc B-foo.sc C-foo.sc".split()
[gc] = self.parse("@range = [A-foo.sc-C-foo.sc];", glyphNames).statements
self.assertEqual(gc.glyphSet(), ("A-foo.sc-C-foo.sc",))
@@ -375,7 +429,10 @@ class ParserTest(unittest.TestCase):
FeatureLibError,
'Ambiguous glyph range "A-B-C"; '
'please use "A - B-C" or "A-B - C" to clarify what you mean',
- self.parse, r"@bad = [A-B-C];", glyphNames)
+ self.parse,
+ r"@bad = [A-B-C];",
+ glyphNames,
+ )
def test_glyphclass_range_digit1(self):
[gc] = self.parse("@range = [foo.2-foo.5];").statements
@@ -392,36 +449,50 @@ class ParserTest(unittest.TestCase):
def test_glyphclass_range_bad(self):
self.assertRaisesRegex(
FeatureLibError,
- "Bad range: \"a\" and \"foobar\" should have the same length",
- self.parse, "@bad = [a-foobar];")
+ 'Bad range: "a" and "foobar" should have the same length',
+ self.parse,
+ "@bad = [a-foobar];",
+ )
self.assertRaisesRegex(
- FeatureLibError, "Bad range: \"A.swash-z.swash\"",
- self.parse, "@bad = [A.swash-z.swash];")
+ FeatureLibError,
+ 'Bad range: "A.swash-z.swash"',
+ self.parse,
+ "@bad = [A.swash-z.swash];",
+ )
self.assertRaisesRegex(
- FeatureLibError, "Start of range must be smaller than its end",
- self.parse, "@bad = [B.swash-A.swash];")
+ FeatureLibError,
+ "Start of range must be smaller than its end",
+ self.parse,
+ "@bad = [B.swash-A.swash];",
+ )
self.assertRaisesRegex(
- FeatureLibError, "Bad range: \"foo.1234-foo.9876\"",
- self.parse, "@bad = [foo.1234-foo.9876];")
+ FeatureLibError,
+ 'Bad range: "foo.1234-foo.9876"',
+ self.parse,
+ "@bad = [foo.1234-foo.9876];",
+ )
def test_glyphclass_range_mixed(self):
[gc] = self.parse("@range = [a foo.09-foo.11 X.sc-Z.sc];").statements
- self.assertEqual(gc.glyphSet(), (
- "a", "foo.09", "foo.10", "foo.11", "X.sc", "Y.sc", "Z.sc"
- ))
+ self.assertEqual(
+ gc.glyphSet(), ("a", "foo.09", "foo.10", "foo.11", "X.sc", "Y.sc", "Z.sc")
+ )
def test_glyphclass_reference(self):
[vowels_lc, vowels_uc, vowels] = self.parse(
"@Vowels.lc = [a e i o u]; @Vowels.uc = [A E I O U];"
- "@Vowels = [@Vowels.lc @Vowels.uc y Y];").statements
+ "@Vowels = [@Vowels.lc @Vowels.uc y Y];"
+ ).statements
self.assertEqual(vowels_lc.glyphSet(), tuple("aeiou"))
self.assertEqual(vowels_uc.glyphSet(), tuple("AEIOU"))
self.assertEqual(vowels.glyphSet(), tuple("aeiouAEIOUyY"))
- self.assertEqual(vowels.asFea(),
- "@Vowels = [@Vowels.lc @Vowels.uc y Y];")
+ self.assertEqual(vowels.asFea(), "@Vowels = [@Vowels.lc @Vowels.uc y Y];")
self.assertRaisesRegex(
- FeatureLibError, "Unknown glyph class @unknown",
- self.parse, "@bad = [@unknown];")
+ FeatureLibError,
+ "Unknown glyph class @unknown",
+ self.parse,
+ "@bad = [@unknown];",
+ )
def test_glyphclass_scoping(self):
[foo, liga, smcp] = self.parse(
@@ -439,8 +510,7 @@ class ParserTest(unittest.TestCase):
"feature F1 { lookup L { @GLYPHCLASS = [A B C];} L; } F1;"
"feature F2 { sub @GLYPHCLASS by D; } F2;"
).statements
- self.assertEqual(list(f2.statements[0].glyphs[0].glyphSet()),
- ["A", "B", "C"])
+ self.assertEqual(list(f2.statements[0].glyphs[0].glyphSet()), ["A", "B", "C"])
def test_GlyphClassDef(self):
doc = self.parse("table GDEF {GlyphClassDef [b],[l],[m],[C c];} GDEF;")
@@ -473,9 +543,8 @@ class ParserTest(unittest.TestCase):
def test_ignore_position(self):
doc = self.parse(
- "feature test {"
- " ignore position f [a e] d' [a u]' [e y];"
- "} test;")
+ "feature test {" " ignore position f [a e] d' [a u]' [e y];" "} test;"
+ )
sub = doc.statements[0].statements[0]
self.assertIsInstance(sub, ast.IgnorePosStatement)
[(prefix, glyphs, suffix)] = sub.chainContexts
@@ -489,7 +558,8 @@ class ParserTest(unittest.TestCase):
'No lookups can be specified for "ignore pos"',
self.parse,
"lookup L { pos [A A.sc] -100; } L;"
- "feature test { ignore pos f' i', A' lookup L; } test;")
+ "feature test { ignore pos f' i', A' lookup L; } test;",
+ )
def test_ignore_sub(self):
doc = self.parse("feature test {ignore sub e t' c, q u' u' x;} test;")
@@ -505,9 +575,8 @@ class ParserTest(unittest.TestCase):
def test_ignore_substitute(self):
doc = self.parse(
- "feature test {"
- " ignore substitute f [a e] d' [a u]' [e y];"
- "} test;")
+ "feature test {" " ignore substitute f [a e] d' [a u]' [e y];" "} test;"
+ )
sub = doc.statements[0].statements[0]
self.assertIsInstance(sub, ast.IgnoreSubstStatement)
[(prefix, glyphs, suffix)] = sub.chainContexts
@@ -521,15 +590,19 @@ class ParserTest(unittest.TestCase):
'No lookups can be specified for "ignore sub"',
self.parse,
"lookup L { sub [A A.sc] by a; } L;"
- "feature test { ignore sub f' i', A' lookup L; } test;")
+ "feature test { ignore sub f' i', A' lookup L; } test;",
+ )
def test_include_statement(self):
- doc = self.parse("""\
+ doc = self.parse(
+ """\
include(../family.fea);
include # Comment
(foo)
;
- """, followIncludes=False)
+ """,
+ followIncludes=False,
+ )
s1, s2, s3 = doc.statements
self.assertEqual(type(s1), ast.IncludeStatement)
self.assertEqual(s1.filename, "../family.fea")
@@ -541,9 +614,12 @@ class ParserTest(unittest.TestCase):
self.assertEqual(s3.text, "# Comment")
def test_include_statement_no_semicolon(self):
- doc = self.parse("""\
+ doc = self.parse(
+ """\
include(../family.fea)
- """, followIncludes=False)
+ """,
+ followIncludes=False,
+ )
s1 = doc.statements[0]
self.assertEqual(type(s1), ast.IncludeStatement)
self.assertEqual(s1.filename, "../family.fea")
@@ -566,9 +642,9 @@ class ParserTest(unittest.TestCase):
self.assertFalse(s.required)
def test_language_exclude_dflt_required(self):
- doc = self.parse("feature test {"
- " language DEU exclude_dflt required;"
- "} test;")
+ doc = self.parse(
+ "feature test {" " language DEU exclude_dflt required;" "} test;"
+ )
s = doc.statements[0].statements[0]
self.assertEqual(type(s), ast.LanguageStatement)
self.assertEqual(s.language, "DEU ")
@@ -584,9 +660,9 @@ class ParserTest(unittest.TestCase):
self.assertFalse(s.required)
def test_language_include_dflt_required(self):
- doc = self.parse("feature test {"
- " language DEU include_dflt required;"
- "} test;")
+ doc = self.parse(
+ "feature test {" " language DEU include_dflt required;" "} test;"
+ )
s = doc.statements[0].statements[0]
self.assertEqual(type(s), ast.LanguageStatement)
self.assertEqual(s.language, "DEU ")
@@ -597,7 +673,9 @@ class ParserTest(unittest.TestCase):
self.assertRaisesRegex(
FeatureLibError,
'"DFLT" is not a valid language tag; use "dflt" instead',
- self.parse, "feature test { language DFLT; } test;")
+ self.parse,
+ "feature test { language DFLT; } test;",
+ )
def test_ligatureCaretByIndex_glyphClass(self):
doc = self.parse("table GDEF{LigatureCaretByIndex [c_t f_i] 2;}GDEF;")
@@ -636,20 +714,21 @@ class ParserTest(unittest.TestCase):
[lookup] = self.parse("lookup Foo useExtension {} Foo;").statements
self.assertEqual(lookup.name, "Foo")
self.assertTrue(lookup.use_extension)
- self.assertEqual(lookup.asFea(),
- "lookup Foo useExtension {\n \n} Foo;\n")
+ self.assertEqual(lookup.asFea(), "lookup Foo useExtension {\n \n} Foo;\n")
def test_lookup_block_name_mismatch(self):
self.assertRaisesRegex(
- FeatureLibError, 'Expected "Foo"',
- self.parse, "lookup Foo {} Bar;")
+ FeatureLibError, 'Expected "Foo"', self.parse, "lookup Foo {} Bar;"
+ )
def test_lookup_block_with_horizontal_valueRecordDef(self):
- doc = self.parse("feature liga {"
- " lookup look {"
- " valueRecordDef 123 foo;"
- " } look;"
- "} liga;")
+ doc = self.parse(
+ "feature liga {"
+ " lookup look {"
+ " valueRecordDef 123 foo;"
+ " } look;"
+ "} liga;"
+ )
[liga] = doc.statements
[look] = liga.statements
[foo] = look.statements
@@ -657,11 +736,13 @@ class ParserTest(unittest.TestCase):
self.assertIsNone(foo.value.yAdvance)
def test_lookup_block_with_vertical_valueRecordDef(self):
- doc = self.parse("feature vkrn {"
- " lookup look {"
- " valueRecordDef 123 foo;"
- " } look;"
- "} vkrn;")
+ doc = self.parse(
+ "feature vkrn {"
+ " lookup look {"
+ " valueRecordDef 123 foo;"
+ " } look;"
+ "} vkrn;"
+ )
[vkrn] = doc.statements
[look] = vkrn.statements
[foo] = look.statements
@@ -675,15 +756,17 @@ class ParserTest(unittest.TestCase):
self.assertEqual(comment.text, "# Comment")
def test_lookup_reference(self):
- [foo, bar] = self.parse("lookup Foo {} Foo;"
- "feature Bar {lookup Foo;} Bar;").statements
+ [foo, bar] = self.parse(
+ "lookup Foo {} Foo;" "feature Bar {lookup Foo;} Bar;"
+ ).statements
[ref] = bar.statements
self.assertEqual(type(ref), ast.LookupReferenceStatement)
self.assertEqual(ref.lookup, foo)
def test_lookup_reference_to_lookup_inside_feature(self):
- [qux, bar] = self.parse("feature Qux {lookup Foo {} Foo;} Qux;"
- "feature Bar {lookup Foo;} Bar;").statements
+ [qux, bar] = self.parse(
+ "feature Qux {lookup Foo {} Foo;} Qux;" "feature Bar {lookup Foo;} Bar;"
+ ).statements
[foo] = qux.statements
[ref] = bar.statements
self.assertIsInstance(ref, ast.LookupReferenceStatement)
@@ -691,8 +774,11 @@ class ParserTest(unittest.TestCase):
def test_lookup_reference_unknown(self):
self.assertRaisesRegex(
- FeatureLibError, 'Unknown lookup "Huh"',
- self.parse, "feature liga {lookup Huh;} liga;")
+ FeatureLibError,
+ 'Unknown lookup "Huh"',
+ self.parse,
+ "feature liga {lookup Huh;} liga;",
+ )
def parse_lookupflag_(self, s):
return self.parse("lookup L {%s} L;" % s).statements[0].statements[-1]
@@ -708,52 +794,59 @@ class ParserTest(unittest.TestCase):
def test_lookupflag_format_A_MarkAttachmentType(self):
flag = self.parse_lookupflag_(
"@TOP_MARKS = [acute grave macron];"
- "lookupflag RightToLeft MarkAttachmentType @TOP_MARKS;")
+ "lookupflag RightToLeft MarkAttachmentType @TOP_MARKS;"
+ )
self.assertIsInstance(flag, ast.LookupFlagStatement)
self.assertEqual(flag.value, 1)
self.assertIsInstance(flag.markAttachment, ast.GlyphClassName)
- self.assertEqual(flag.markAttachment.glyphSet(),
- ("acute", "grave", "macron"))
+ self.assertEqual(flag.markAttachment.glyphSet(), ("acute", "grave", "macron"))
self.assertIsNone(flag.markFilteringSet)
- self.assertEqual(flag.asFea(),
- "lookupflag RightToLeft MarkAttachmentType @TOP_MARKS;")
+ self.assertEqual(
+ flag.asFea(), "lookupflag RightToLeft MarkAttachmentType @TOP_MARKS;"
+ )
def test_lookupflag_format_A_MarkAttachmentType_glyphClass(self):
flag = self.parse_lookupflag_(
- "lookupflag RightToLeft MarkAttachmentType [acute grave macron];")
+ "lookupflag RightToLeft MarkAttachmentType [acute grave macron];"
+ )
self.assertIsInstance(flag, ast.LookupFlagStatement)
self.assertEqual(flag.value, 1)
self.assertIsInstance(flag.markAttachment, ast.GlyphClass)
- self.assertEqual(flag.markAttachment.glyphSet(),
- ("acute", "grave", "macron"))
+ self.assertEqual(flag.markAttachment.glyphSet(), ("acute", "grave", "macron"))
self.assertIsNone(flag.markFilteringSet)
- self.assertEqual(flag.asFea(),
- "lookupflag RightToLeft MarkAttachmentType [acute grave macron];")
+ self.assertEqual(
+ flag.asFea(),
+ "lookupflag RightToLeft MarkAttachmentType [acute grave macron];",
+ )
def test_lookupflag_format_A_UseMarkFilteringSet(self):
flag = self.parse_lookupflag_(
"@BOTTOM_MARKS = [cedilla ogonek];"
- "lookupflag UseMarkFilteringSet @BOTTOM_MARKS IgnoreLigatures;")
+ "lookupflag UseMarkFilteringSet @BOTTOM_MARKS IgnoreLigatures;"
+ )
self.assertIsInstance(flag, ast.LookupFlagStatement)
self.assertEqual(flag.value, 4)
self.assertIsNone(flag.markAttachment)
self.assertIsInstance(flag.markFilteringSet, ast.GlyphClassName)
- self.assertEqual(flag.markFilteringSet.glyphSet(),
- ("cedilla", "ogonek"))
- self.assertEqual(flag.asFea(),
- "lookupflag IgnoreLigatures UseMarkFilteringSet @BOTTOM_MARKS;")
+ self.assertEqual(flag.markFilteringSet.glyphSet(), ("cedilla", "ogonek"))
+ self.assertEqual(
+ flag.asFea(),
+ "lookupflag IgnoreLigatures UseMarkFilteringSet @BOTTOM_MARKS;",
+ )
def test_lookupflag_format_A_UseMarkFilteringSet_glyphClass(self):
flag = self.parse_lookupflag_(
- "lookupflag UseMarkFilteringSet [cedilla ogonek] IgnoreLigatures;")
+ "lookupflag UseMarkFilteringSet [cedilla ogonek] IgnoreLigatures;"
+ )
self.assertIsInstance(flag, ast.LookupFlagStatement)
self.assertEqual(flag.value, 4)
self.assertIsNone(flag.markAttachment)
self.assertIsInstance(flag.markFilteringSet, ast.GlyphClass)
- self.assertEqual(flag.markFilteringSet.glyphSet(),
- ("cedilla", "ogonek"))
- self.assertEqual(flag.asFea(),
- "lookupflag IgnoreLigatures UseMarkFilteringSet [cedilla ogonek];")
+ self.assertEqual(flag.markFilteringSet.glyphSet(), ("cedilla", "ogonek"))
+ self.assertEqual(
+ flag.asFea(),
+ "lookupflag IgnoreLigatures UseMarkFilteringSet [cedilla ogonek];",
+ )
def test_lookupflag_format_B(self):
flag = self.parse_lookupflag_("lookupflag 7;")
@@ -761,8 +854,9 @@ class ParserTest(unittest.TestCase):
self.assertEqual(flag.value, 7)
self.assertIsNone(flag.markAttachment)
self.assertIsNone(flag.markFilteringSet)
- self.assertEqual(flag.asFea(),
- "lookupflag RightToLeft IgnoreBaseGlyphs IgnoreLigatures;")
+ self.assertEqual(
+ flag.asFea(), "lookupflag RightToLeft IgnoreBaseGlyphs IgnoreLigatures;"
+ )
def test_lookupflag_format_B_zero(self):
flag = self.parse_lookupflag_("lookupflag 0;")
@@ -775,22 +869,26 @@ class ParserTest(unittest.TestCase):
def test_lookupflag_no_value(self):
self.assertRaisesRegex(
FeatureLibError,
- 'lookupflag must have a value',
+ "lookupflag must have a value",
self.parse,
- "feature test {lookupflag;} test;")
+ "feature test {lookupflag;} test;",
+ )
def test_lookupflag_repeated(self):
self.assertRaisesRegex(
FeatureLibError,
- 'RightToLeft can be specified only once',
+ "RightToLeft can be specified only once",
self.parse,
- "feature test {lookupflag RightToLeft RightToLeft;} test;")
+ "feature test {lookupflag RightToLeft RightToLeft;} test;",
+ )
def test_lookupflag_unrecognized(self):
self.assertRaisesRegex(
FeatureLibError,
'"IgnoreCookies" is not a recognized lookupflag',
- self.parse, "feature test {lookupflag IgnoreCookies;} test;")
+ self.parse,
+ "feature test {lookupflag IgnoreCookies;} test;",
+ )
def test_gpos_type_1_glyph(self):
doc = self.parse("feature kern {pos one <1 2 3 4>;} kern;")
@@ -834,11 +932,15 @@ class ParserTest(unittest.TestCase):
self.assertRaisesRegex(
FeatureLibError,
'"enumerate" is only allowed with pair positionings',
- self.parse, "feature test {enum pos T 100;} test;")
+ self.parse,
+ "feature test {enum pos T 100;} test;",
+ )
self.assertRaisesRegex(
FeatureLibError,
'"enumerate" is only allowed with pair positionings',
- self.parse, "feature test {enumerate pos T 100;} test;")
+ self.parse,
+ "feature test {enumerate pos T 100;} test;",
+ )
def test_gpos_type_1_chained(self):
doc = self.parse("feature kern {pos [A B] [T Y]' 20 comma;} kern;")
@@ -870,10 +972,47 @@ class ParserTest(unittest.TestCase):
self.assertEqual(glyphstr(pos.prefix), "[A B]")
self.assertEqual(glyphstr(pos.suffix), "comma")
+ def test_gpos_type_1_chained_special_kern_format_valuerecord_format_b_bug2293(self):
+ # https://github.com/fonttools/fonttools/issues/2293
+ doc = self.parse("feature kern {pos [A B] [T Y]' comma a <0 0 0 0>;} kern;")
+ pos = doc.statements[0].statements[0]
+ self.assertIsInstance(pos, ast.SinglePosStatement)
+ [(glyphs, value)] = pos.pos
+ self.assertEqual(glyphstr([glyphs]), "[T Y]")
+ self.assertEqual(value.asFea(), "<0 0 0 0>")
+ self.assertEqual(glyphstr(pos.prefix), "[A B]")
+ self.assertEqual(glyphstr(pos.suffix), "comma a")
+
+ def test_gpos_type_1_chained_exception1(self):
+ with self.assertRaisesRegex(FeatureLibError, "Positioning values are allowed"):
+ doc = self.parse(
+ "feature kern {" " pos [A B]' [T Y]' comma a <0 0 0 0>;" "} kern;"
+ )
+
+ def test_gpos_type_1_chained_exception2(self):
+ with self.assertRaisesRegex(FeatureLibError, "Positioning values are allowed"):
+ doc = self.parse(
+ "feature kern {"
+ " pos [A B]' <0 0 0 0> [T Y]' comma a <0 0 0 0>;"
+ "} kern;"
+ )
+
+ def test_gpos_type_1_chained_exception3(self):
+ with self.assertRaisesRegex(FeatureLibError, "Positioning cannot be applied"):
+ doc = self.parse(
+ "feature kern {"
+ " pos [A B] <0 0 0 0> [T Y]' comma a <0 0 0 0>;"
+ "} kern;"
+ )
+
+ def test_gpos_type_1_chained_exception4(self):
+ with self.assertRaisesRegex(FeatureLibError, "Positioning values are allowed"):
+ doc = self.parse("feature kern {" " pos a' b c 123 d;" "} kern;")
+
def test_gpos_type_2_format_a(self):
- doc = self.parse("feature kern {"
- " pos [T V] -60 [a b c] <1 2 3 4>;"
- "} kern;")
+ doc = self.parse(
+ "feature kern {" " pos [T V] -60 [a b c] <1 2 3 4>;" "} kern;"
+ )
pos = doc.statements[0].statements[0]
self.assertEqual(type(pos), ast.PairPosStatement)
self.assertFalse(pos.enumerated)
@@ -883,9 +1022,9 @@ class ParserTest(unittest.TestCase):
self.assertEqual(pos.valuerecord2.asFea(), "<1 2 3 4>")
def test_gpos_type_2_format_a_enumerated(self):
- doc = self.parse("feature kern {"
- " enum pos [T V] -60 [a b c] <1 2 3 4>;"
- "} kern;")
+ doc = self.parse(
+ "feature kern {" " enum pos [T V] -60 [a b c] <1 2 3 4>;" "} kern;"
+ )
pos = doc.statements[0].statements[0]
self.assertEqual(type(pos), ast.PairPosStatement)
self.assertTrue(pos.enumerated)
@@ -895,9 +1034,9 @@ class ParserTest(unittest.TestCase):
self.assertEqual(pos.valuerecord2.asFea(), "<1 2 3 4>")
def test_gpos_type_2_format_a_with_null_first(self):
- doc = self.parse("feature kern {"
- " pos [T V] <NULL> [a b c] <1 2 3 4>;"
- "} kern;")
+ doc = self.parse(
+ "feature kern {" " pos [T V] <NULL> [a b c] <1 2 3 4>;" "} kern;"
+ )
pos = doc.statements[0].statements[0]
self.assertEqual(type(pos), ast.PairPosStatement)
self.assertFalse(pos.enumerated)
@@ -909,9 +1048,9 @@ class ParserTest(unittest.TestCase):
self.assertEqual(pos.asFea(), "pos [T V] <NULL> [a b c] <1 2 3 4>;")
def test_gpos_type_2_format_a_with_null_second(self):
- doc = self.parse("feature kern {"
- " pos [T V] <1 2 3 4> [a b c] <NULL>;"
- "} kern;")
+ doc = self.parse(
+ "feature kern {" " pos [T V] <1 2 3 4> [a b c] <NULL>;" "} kern;"
+ )
pos = doc.statements[0].statements[0]
self.assertEqual(type(pos), ast.PairPosStatement)
self.assertFalse(pos.enumerated)
@@ -922,9 +1061,7 @@ class ParserTest(unittest.TestCase):
self.assertEqual(pos.asFea(), "pos [T V] [a b c] <1 2 3 4>;")
def test_gpos_type_2_format_b(self):
- doc = self.parse("feature kern {"
- " pos [T V] [a b c] <1 2 3 4>;"
- "} kern;")
+ doc = self.parse("feature kern {" " pos [T V] [a b c] <1 2 3 4>;" "} kern;")
pos = doc.statements[0].statements[0]
self.assertEqual(type(pos), ast.PairPosStatement)
self.assertFalse(pos.enumerated)
@@ -934,9 +1071,9 @@ class ParserTest(unittest.TestCase):
self.assertIsNone(pos.valuerecord2)
def test_gpos_type_2_format_b_enumerated(self):
- doc = self.parse("feature kern {"
- " enumerate position [T V] [a b c] <1 2 3 4>;"
- "} kern;")
+ doc = self.parse(
+ "feature kern {" " enumerate position [T V] [a b c] <1 2 3 4>;" "} kern;"
+ )
pos = doc.statements[0].statements[0]
self.assertEqual(type(pos), ast.PairPosStatement)
self.assertTrue(pos.enumerated)
@@ -946,9 +1083,11 @@ class ParserTest(unittest.TestCase):
self.assertIsNone(pos.valuerecord2)
def test_gpos_type_3(self):
- doc = self.parse("feature kern {"
- " position cursive A <anchor 12 -2> <anchor 2 3>;"
- "} kern;")
+ doc = self.parse(
+ "feature kern {"
+ " position cursive A <anchor 12 -2> <anchor 2 3>;"
+ "} kern;"
+ )
pos = doc.statements[0].statements[0]
self.assertEqual(type(pos), ast.CursivePosStatement)
self.assertEqual(pos.glyphclass.glyphSet(), ("A",))
@@ -962,7 +1101,8 @@ class ParserTest(unittest.TestCase):
self.parse,
"feature kern {"
" enumerate position cursive A <anchor 12 -2> <anchor 2 3>;"
- "} kern;")
+ "} kern;",
+ )
def test_gpos_type_4(self):
doc = self.parse(
@@ -973,7 +1113,8 @@ class ParserTest(unittest.TestCase):
" position base [a e o u] "
" <anchor 250 450> mark @TOP_MARKS "
" <anchor 210 -10> mark @BOTTOM_MARKS;"
- "} test;")
+ "} test;"
+ )
pos = doc.statements[-1].statements[0]
self.assertEqual(type(pos), ast.MarkBasePosStatement)
self.assertEqual(pos.base.glyphSet(), ("a", "e", "o", "u"))
@@ -984,21 +1125,24 @@ class ParserTest(unittest.TestCase):
def test_gpos_type_4_enumerated(self):
self.assertRaisesRegex(
FeatureLibError,
- '"enumerate" is not allowed with '
- 'mark-to-base attachment positioning',
+ '"enumerate" is not allowed with ' "mark-to-base attachment positioning",
self.parse,
"feature kern {"
" markClass cedilla <anchor 300 600> @BOTTOM_MARKS;"
" enumerate position base A <anchor 12 -2> mark @BOTTOM_MARKS;"
- "} kern;")
+ "} kern;",
+ )
def test_gpos_type_4_not_markClass(self):
self.assertRaisesRegex(
- FeatureLibError, "@MARKS is not a markClass", self.parse,
+ FeatureLibError,
+ "@MARKS is not a markClass",
+ self.parse,
"@MARKS = [acute grave];"
"feature test {"
" position base [a e o u] <anchor 250 450> mark @MARKS;"
- "} test;")
+ "} test;",
+ )
def test_gpos_type_5(self):
doc = self.parse(
@@ -1015,7 +1159,8 @@ class ParserTest(unittest.TestCase):
" <anchor NULL> "
" ligComponent "
" <anchor 30 -10> mark @BOTTOM_MARKS;"
- "} test;")
+ "} test;"
+ )
pos = doc.statements[-1].statements[0]
self.assertEqual(type(pos), ast.MarkLigPosStatement)
self.assertEqual(pos.ligatures.glyphSet(), ("a_f_f_i", "o_f_f_i"))
@@ -1029,29 +1174,34 @@ class ParserTest(unittest.TestCase):
self.assertRaisesRegex(
FeatureLibError,
'"enumerate" is not allowed with '
- 'mark-to-ligature attachment positioning',
+ "mark-to-ligature attachment positioning",
self.parse,
"feature test {"
" markClass cedilla <anchor 300 600> @MARKS;"
" enumerate position "
" ligature f_i <anchor 100 0> mark @MARKS"
" ligComponent <anchor NULL>;"
- "} test;")
+ "} test;",
+ )
def test_gpos_type_5_not_markClass(self):
self.assertRaisesRegex(
- FeatureLibError, "@MARKS is not a markClass", self.parse,
+ FeatureLibError,
+ "@MARKS is not a markClass",
+ self.parse,
"@MARKS = [acute grave];"
"feature test {"
" position ligature f_i <anchor 250 450> mark @MARKS;"
- "} test;")
+ "} test;",
+ )
def test_gpos_type_6(self):
doc = self.parse(
"markClass damma <anchor 189 -103> @MARK_CLASS_1;"
"feature test {"
" position mark hamza <anchor 221 301> mark @MARK_CLASS_1;"
- "} test;")
+ "} test;"
+ )
pos = doc.statements[-1].statements[0]
self.assertEqual(type(pos), ast.MarkMarkPosStatement)
self.assertEqual(pos.baseMarks.glyphSet(), ("hamza",))
@@ -1061,28 +1211,32 @@ class ParserTest(unittest.TestCase):
def test_gpos_type_6_enumerated(self):
self.assertRaisesRegex(
FeatureLibError,
- '"enumerate" is not allowed with '
- 'mark-to-mark attachment positioning',
+ '"enumerate" is not allowed with ' "mark-to-mark attachment positioning",
self.parse,
"markClass damma <anchor 189 -103> @MARK_CLASS_1;"
"feature test {"
" enum pos mark hamza <anchor 221 301> mark @MARK_CLASS_1;"
- "} test;")
+ "} test;",
+ )
def test_gpos_type_6_not_markClass(self):
self.assertRaisesRegex(
- FeatureLibError, "@MARKS is not a markClass", self.parse,
+ FeatureLibError,
+ "@MARKS is not a markClass",
+ self.parse,
"@MARKS = [acute grave];"
"feature test {"
" position mark cedilla <anchor 250 450> mark @MARKS;"
- "} test;")
+ "} test;",
+ )
def test_gpos_type_8(self):
doc = self.parse(
"lookup L1 {pos one 100;} L1; lookup L2 {pos two 200;} L2;"
"feature test {"
" pos [A a] [B b] I' lookup L1 [N n]' lookup L2 P' [Y y] [Z z];"
- "} test;")
+ "} test;"
+ )
lookup1, lookup2 = doc.statements[0:2]
pos = doc.statements[-1].statements[0]
self.assertEqual(type(pos), ast.ChainContextPosStatement)
@@ -1099,7 +1253,8 @@ class ParserTest(unittest.TestCase):
"lookup L1 {pos one 100;} L1;"
"feature test {"
" pos A' lookup L1 B' 20;"
- "} test;")
+ "} test;",
+ )
def test_markClass(self):
doc = self.parse("markClass [acute grave] <anchor 350 3> @MARKS;")
@@ -1110,8 +1265,7 @@ class ParserTest(unittest.TestCase):
self.assertEqual((mc.anchor.x, mc.anchor.y), (350, 3))
def test_nameid_windows_utf16(self):
- doc = self.parse(
- r'table name { nameid 9 "M\00fcller-Lanc\00e9"; } name;')
+ doc = self.parse(r'table name { nameid 9 "M\00fcller-Lanc\00e9"; } name;')
name = doc.statements[0].statements[0]
self.assertIsInstance(name, ast.NameRecord)
self.assertEqual(name.nameID, 9)
@@ -1128,8 +1282,7 @@ class ParserTest(unittest.TestCase):
self.assertEqual(name.asFea(), r'nameid 9 "Back\005cslash";')
def test_nameid_windows_utf16_quotation_mark(self):
- doc = self.parse(
- r'table name { nameid 9 "Quotation \0022Mark\0022"; } name;')
+ doc = self.parse(r'table name { nameid 9 "Quotation \0022Mark\0022"; } name;')
name = doc.statements[0].statements[0]
self.assertEqual(name.string, 'Quotation "Mark"')
self.assertEqual(name.asFea(), r'nameid 9 "Quotation \0022Mark\0022";')
@@ -1141,8 +1294,7 @@ class ParserTest(unittest.TestCase):
self.assertEqual(name.asFea(), r'nameid 9 "Carrot \d83e\dd55";')
def test_nameid_mac_roman(self):
- doc = self.parse(
- r'table name { nameid 9 1 "Joachim M\9fller-Lanc\8e"; } name;')
+ doc = self.parse(r'table name { nameid 9 1 "Joachim M\9fller-Lanc\8e"; } name;')
name = doc.statements[0].statements[0]
self.assertIsInstance(name, ast.NameRecord)
self.assertEqual(name.nameID, 9)
@@ -1150,12 +1302,10 @@ class ParserTest(unittest.TestCase):
self.assertEqual(name.platEncID, 0)
self.assertEqual(name.langID, 0)
self.assertEqual(name.string, "Joachim Müller-Lancé")
- self.assertEqual(name.asFea(),
- r'nameid 9 1 "Joachim M\9fller-Lanc\8e";')
+ self.assertEqual(name.asFea(), r'nameid 9 1 "Joachim M\9fller-Lanc\8e";')
def test_nameid_mac_croatian(self):
- doc = self.parse(
- r'table name { nameid 9 1 0 18 "Jovica Veljovi\e6"; } name;')
+ doc = self.parse(r'table name { nameid 9 1 0 18 "Jovica Veljovi\e6"; } name;')
name = doc.statements[0].statements[0]
self.assertEqual(name.nameID, 9)
self.assertEqual(name.platformID, 1)
@@ -1166,12 +1316,14 @@ class ParserTest(unittest.TestCase):
def test_nameid_unsupported_platform(self):
self.assertRaisesRegex(
- FeatureLibError, "Expected platform id 1 or 3",
- self.parse, 'table name { nameid 9 666 "Foo"; } name;')
+ FeatureLibError,
+ "Expected platform id 1 or 3",
+ self.parse,
+ 'table name { nameid 9 666 "Foo"; } name;',
+ )
def test_nameid_hexadecimal(self):
- doc = self.parse(
- r'table name { nameid 0x9 0x3 0x1 0x0409 "Test"; } name;')
+ doc = self.parse(r'table name { nameid 0x9 0x3 0x1 0x0409 "Test"; } name;')
name = doc.statements[0].statements[0]
self.assertEqual(name.nameID, 9)
self.assertEqual(name.platformID, 3)
@@ -1179,8 +1331,7 @@ class ParserTest(unittest.TestCase):
self.assertEqual(name.langID, 0x0409)
def test_nameid_octal(self):
- doc = self.parse(
- r'table name { nameid 011 03 012 02011 "Test"; } name;')
+ doc = self.parse(r'table name { nameid 011 03 012 02011 "Test"; } name;')
name = doc.statements[0].statements[0]
self.assertEqual(name.nameID, 9)
self.assertEqual(name.platformID, 3)
@@ -1188,14 +1339,12 @@ class ParserTest(unittest.TestCase):
self.assertEqual(name.langID, 0o2011)
def test_cv_hexadecimal(self):
- doc = self.parse(
- r'feature cv01 { cvParameters { Character 0x5DDE; }; } cv01;')
+ doc = self.parse(r"feature cv01 { cvParameters { Character 0x5DDE; }; } cv01;")
cv = doc.statements[0].statements[0].statements[0]
self.assertEqual(cv.character, 0x5DDE)
def test_cv_octal(self):
- doc = self.parse(
- r'feature cv01 { cvParameters { Character 056736; }; } cv01;')
+ doc = self.parse(r"feature cv01 { cvParameters { Character 056736; }; } cv01;")
cv = doc.statements[0].statements[0].statements[0]
self.assertEqual(cv.character, 0o56736)
@@ -1212,8 +1361,7 @@ class ParserTest(unittest.TestCase):
doc = self.parse(r"feature test {rsub \1 [\2 \3] \4' \5 by \6;} test;")
rsub = doc.statements[0].statements[0]
self.assertEqual(type(rsub), ast.ReverseChainSingleSubstStatement)
- self.assertEqual(glyphstr(rsub.old_prefix),
- "cid00001 [cid00002 cid00003]")
+ self.assertEqual(glyphstr(rsub.old_prefix), "cid00001 [cid00002 cid00003]")
self.assertEqual(rsub.glyphs[0].glyphSet(), ("cid00004",))
self.assertEqual(rsub.replacements[0].glyphSet(), ("cid00006",))
self.assertEqual(glyphstr(rsub.old_suffix), "cid00005")
@@ -1222,51 +1370,53 @@ class ParserTest(unittest.TestCase):
doc = self.parse(
"feature smcp {"
" reversesub A B [one.fitted one.oldstyle]' C [d D] by one;"
- "} smcp;")
+ "} smcp;"
+ )
rsub = doc.statements[0].statements[0]
self.assertEqual(type(rsub), ast.ReverseChainSingleSubstStatement)
self.assertEqual(glyphstr(rsub.old_prefix), "A B")
self.assertEqual(glyphstr(rsub.old_suffix), "C [D d]")
- self.assertEqual(mapping(rsub), {
- "one.fitted": "one",
- "one.oldstyle": "one"
- })
+ self.assertEqual(mapping(rsub), {"one.fitted": "one", "one.oldstyle": "one"})
def test_rsub_format_c(self):
doc = self.parse(
"feature test {"
" reversesub BACK TRACK [a-d]' LOOK AHEAD by [A.sc-D.sc];"
- "} test;")
+ "} test;"
+ )
rsub = doc.statements[0].statements[0]
self.assertEqual(type(rsub), ast.ReverseChainSingleSubstStatement)
self.assertEqual(glyphstr(rsub.old_prefix), "BACK TRACK")
self.assertEqual(glyphstr(rsub.old_suffix), "LOOK AHEAD")
- self.assertEqual(mapping(rsub), {
- "a": "A.sc",
- "b": "B.sc",
- "c": "C.sc",
- "d": "D.sc"
- })
+ self.assertEqual(
+ mapping(rsub), {"a": "A.sc", "b": "B.sc", "c": "C.sc", "d": "D.sc"}
+ )
def test_rsub_from(self):
self.assertRaisesRegex(
FeatureLibError,
'Reverse chaining substitutions do not support "from"',
- self.parse, "feature test {rsub a from [a.1 a.2 a.3];} test;")
+ self.parse,
+ "feature test {rsub a from [a.1 a.2 a.3];} test;",
+ )
def test_rsub_nonsingle(self):
self.assertRaisesRegex(
FeatureLibError,
"In reverse chaining single substitutions, only a single glyph "
"or glyph class can be replaced",
- self.parse, "feature test {rsub c d by c_d;} test;")
+ self.parse,
+ "feature test {rsub c d by c_d;} test;",
+ )
def test_rsub_multiple_replacement_glyphs(self):
self.assertRaisesRegex(
FeatureLibError,
- 'In reverse chaining single substitutions, the replacement '
+ "In reverse chaining single substitutions, the replacement "
r'\(after "by"\) must be a single glyph or glyph class',
- self.parse, "feature test {rsub f_i by f i;} test;")
+ self.parse,
+ "feature test {rsub f_i by f i;} test;",
+ )
def test_script(self):
doc = self.parse("feature test {script cyrl;} test;")
@@ -1278,77 +1428,92 @@ class ParserTest(unittest.TestCase):
self.assertRaisesRegex(
FeatureLibError,
'"dflt" is not a valid script tag; use "DFLT" instead',
- self.parse, "feature test {script dflt;} test;")
+ self.parse,
+ "feature test {script dflt;} test;",
+ )
def test_stat_design_axis(self): # STAT DesignAxis
- doc = self.parse('table STAT { DesignAxis opsz 0 '
- '{name "Optical Size";}; } STAT;')
+ doc = self.parse(
+ "table STAT { DesignAxis opsz 0 " '{name "Optical Size";}; } STAT;'
+ )
da = doc.statements[0].statements[0]
self.assertIsInstance(da, ast.STATDesignAxisStatement)
- self.assertEqual(da.tag, 'opsz')
+ self.assertEqual(da.tag, "opsz")
self.assertEqual(da.axisOrder, 0)
- self.assertEqual(da.names[0].string, 'Optical Size')
+ self.assertEqual(da.names[0].string, "Optical Size")
def test_stat_axis_value_format1(self): # STAT AxisValue
- doc = self.parse('table STAT { DesignAxis opsz 0 '
- '{name "Optical Size";}; '
- 'AxisValue {location opsz 8; name "Caption";}; } '
- 'STAT;')
+ doc = self.parse(
+ "table STAT { DesignAxis opsz 0 "
+ '{name "Optical Size";}; '
+ 'AxisValue {location opsz 8; name "Caption";}; } '
+ "STAT;"
+ )
avr = doc.statements[0].statements[1]
self.assertIsInstance(avr, ast.STATAxisValueStatement)
- self.assertEqual(avr.locations[0].tag, 'opsz')
+ self.assertEqual(avr.locations[0].tag, "opsz")
self.assertEqual(avr.locations[0].values[0], 8)
- self.assertEqual(avr.names[0].string, 'Caption')
+ self.assertEqual(avr.names[0].string, "Caption")
def test_stat_axis_value_format2(self): # STAT AxisValue
- doc = self.parse('table STAT { DesignAxis opsz 0 '
- '{name "Optical Size";}; '
- 'AxisValue {location opsz 8 6 10; name "Caption";}; } '
- 'STAT;')
+ doc = self.parse(
+ "table STAT { DesignAxis opsz 0 "
+ '{name "Optical Size";}; '
+ 'AxisValue {location opsz 8 6 10; name "Caption";}; } '
+ "STAT;"
+ )
avr = doc.statements[0].statements[1]
self.assertIsInstance(avr, ast.STATAxisValueStatement)
- self.assertEqual(avr.locations[0].tag, 'opsz')
+ self.assertEqual(avr.locations[0].tag, "opsz")
self.assertEqual(avr.locations[0].values, [8, 6, 10])
- self.assertEqual(avr.names[0].string, 'Caption')
+ self.assertEqual(avr.names[0].string, "Caption")
def test_stat_axis_value_format2_bad_range(self): # STAT AxisValue
self.assertRaisesRegex(
FeatureLibError,
- 'Default value 5 is outside of specified range 6-10.',
- self.parse, 'table STAT { DesignAxis opsz 0 '
- '{name "Optical Size";}; '
- 'AxisValue {location opsz 5 6 10; name "Caption";}; } '
- 'STAT;')
+ "Default value 5 is outside of specified range 6-10.",
+ self.parse,
+ "table STAT { DesignAxis opsz 0 "
+ '{name "Optical Size";}; '
+ 'AxisValue {location opsz 5 6 10; name "Caption";}; } '
+ "STAT;",
+ )
def test_stat_axis_value_format4(self): # STAT AxisValue
self.assertRaisesRegex(
FeatureLibError,
- 'Only one value is allowed in a Format 4 Axis Value Record, but 3 were found.',
- self.parse, 'table STAT { '
- 'DesignAxis opsz 0 {name "Optical Size";}; '
- 'DesignAxis wdth 0 {name "Width";}; '
- 'AxisValue {'
- 'location opsz 8 6 10; '
- 'location wdth 400; '
- 'name "Caption";}; } '
- 'STAT;')
+ "Only one value is allowed in a Format 4 Axis Value Record, but 3 were found.",
+ self.parse,
+ "table STAT { "
+ 'DesignAxis opsz 0 {name "Optical Size";}; '
+ 'DesignAxis wdth 0 {name "Width";}; '
+ "AxisValue {"
+ "location opsz 8 6 10; "
+ "location wdth 400; "
+ 'name "Caption";}; } '
+ "STAT;",
+ )
def test_stat_elidedfallbackname(self): # STAT ElidedFallbackName
- doc = self.parse('table STAT { ElidedFallbackName {name "Roman"; '
- 'name 3 1 0x0411 "ローマン"; }; '
- '} STAT;')
+ doc = self.parse(
+ 'table STAT { ElidedFallbackName {name "Roman"; '
+ 'name 3 1 0x0411 "ローマン"; }; '
+ "} STAT;"
+ )
nameRecord = doc.statements[0].statements[0]
self.assertIsInstance(nameRecord, ast.ElidedFallbackName)
- self.assertEqual(nameRecord.names[0].string, 'Roman')
- self.assertEqual(nameRecord.names[1].string, 'ローマン')
+ self.assertEqual(nameRecord.names[0].string, "Roman")
+ self.assertEqual(nameRecord.names[1].string, "ローマン")
def test_stat_elidedfallbacknameid(self): # STAT ElidedFallbackNameID
- doc = self.parse('table name { nameid 278 "Roman"; } name; '
- 'table STAT { ElidedFallbackNameID 278; '
- '} STAT;')
+ doc = self.parse(
+ 'table name { nameid 278 "Roman"; } name; '
+ "table STAT { ElidedFallbackNameID 278; "
+ "} STAT;"
+ )
nameRecord = doc.statements[0].statements[0]
self.assertIsInstance(nameRecord, ast.NameRecord)
- self.assertEqual(nameRecord.string, 'Roman')
+ self.assertEqual(nameRecord.string, "Roman")
def test_sub_single_format_a(self): # GSUB LookupType 1
doc = self.parse("feature smcp {substitute a by a.sc;} smcp;")
@@ -1378,13 +1543,11 @@ class ParserTest(unittest.TestCase):
doc = self.parse(
"feature smcp {"
" substitute [one.fitted one.oldstyle] by one;"
- "} smcp;")
+ "} smcp;"
+ )
sub = doc.statements[0].statements[0]
self.assertIsInstance(sub, ast.SingleSubstStatement)
- self.assertEqual(mapping(sub), {
- "one.fitted": "one",
- "one.oldstyle": "one"
- })
+ self.assertEqual(mapping(sub), {"one.fitted": "one", "one.oldstyle": "one"})
self.assertEqual(glyphstr(sub.prefix), "")
self.assertEqual(glyphstr(sub.suffix), "")
@@ -1392,45 +1555,35 @@ class ParserTest(unittest.TestCase):
doc = self.parse(
"feature smcp {"
" substitute PRE FIX [one.fitted one.oldstyle]' SUF FIX by one;"
- "} smcp;")
+ "} smcp;"
+ )
sub = doc.statements[0].statements[0]
self.assertIsInstance(sub, ast.SingleSubstStatement)
- self.assertEqual(mapping(sub), {
- "one.fitted": "one",
- "one.oldstyle": "one"
- })
+ self.assertEqual(mapping(sub), {"one.fitted": "one", "one.oldstyle": "one"})
self.assertEqual(glyphstr(sub.prefix), "PRE FIX")
self.assertEqual(glyphstr(sub.suffix), "SUF FIX")
def test_sub_single_format_c(self): # GSUB LookupType 1
doc = self.parse(
- "feature smcp {"
- " substitute [a-d] by [A.sc-D.sc];"
- "} smcp;")
+ "feature smcp {" " substitute [a-d] by [A.sc-D.sc];" "} smcp;"
+ )
sub = doc.statements[0].statements[0]
self.assertIsInstance(sub, ast.SingleSubstStatement)
- self.assertEqual(mapping(sub), {
- "a": "A.sc",
- "b": "B.sc",
- "c": "C.sc",
- "d": "D.sc"
- })
+ self.assertEqual(
+ mapping(sub), {"a": "A.sc", "b": "B.sc", "c": "C.sc", "d": "D.sc"}
+ )
self.assertEqual(glyphstr(sub.prefix), "")
self.assertEqual(glyphstr(sub.suffix), "")
def test_sub_single_format_c_chained(self): # chain to GSUB LookupType 1
doc = self.parse(
- "feature smcp {"
- " substitute [a-d]' X Y [Z z] by [A.sc-D.sc];"
- "} smcp;")
+ "feature smcp {" " substitute [a-d]' X Y [Z z] by [A.sc-D.sc];" "} smcp;"
+ )
sub = doc.statements[0].statements[0]
self.assertIsInstance(sub, ast.SingleSubstStatement)
- self.assertEqual(mapping(sub), {
- "a": "A.sc",
- "b": "B.sc",
- "c": "C.sc",
- "d": "D.sc"
- })
+ self.assertEqual(
+ mapping(sub), {"a": "A.sc", "b": "B.sc", "c": "C.sc", "d": "D.sc"}
+ )
self.assertEqual(glyphstr(sub.prefix), "")
self.assertEqual(glyphstr(sub.suffix), "X Y [Z z]")
@@ -1438,14 +1591,18 @@ class ParserTest(unittest.TestCase):
self.assertRaisesRegex(
FeatureLibError,
'Expected a glyph class with 4 elements after "by", '
- 'but found a glyph class with 26 elements',
- self.parse, "feature smcp {sub [a-d] by [A.sc-Z.sc];} smcp;")
+ "but found a glyph class with 26 elements",
+ self.parse,
+ "feature smcp {sub [a-d] by [A.sc-Z.sc];} smcp;",
+ )
def test_sub_with_values(self):
self.assertRaisesRegex(
FeatureLibError,
"Substitution statements cannot contain values",
- self.parse, "feature smcp {sub A' 20 by A.sc;} smcp;")
+ self.parse,
+ "feature smcp {sub A' 20 by A.sc;} smcp;",
+ )
def test_substitute_multiple(self): # GSUB LookupType 2
doc = self.parse("lookup Look {substitute f_f_i by f f i;} Look;")
@@ -1475,52 +1632,59 @@ class ParserTest(unittest.TestCase):
"Direct substitution of multiple glyphs by multiple glyphs "
"is not supported",
self.parse,
- "lookup MxM {sub a b c by d e f;} MxM;")
+ "lookup MxM {sub a b c by d e f;} MxM;",
+ )
def test_split_marked_glyphs_runs(self):
self.assertRaisesRegex(
FeatureLibError,
"Unsupported contextual target sequence",
- self.parse, "feature test{"
- " ignore pos a' x x A';"
- "} test;")
+ self.parse,
+ "feature test{" " ignore pos a' x x A';" "} test;",
+ )
self.assertRaisesRegex(
FeatureLibError,
"Unsupported contextual target sequence",
- self.parse, "lookup shift {"
- " pos a <0 -10 0 0>;"
- " pos A <0 10 0 0>;"
- "} shift;"
- "feature test {"
- " sub a' lookup shift x x A' lookup shift;"
- "} test;")
+ self.parse,
+ "lookup shift {"
+ " pos a <0 -10 0 0>;"
+ " pos A <0 10 0 0>;"
+ "} shift;"
+ "feature test {"
+ " sub a' lookup shift x x A' lookup shift;"
+ "} test;",
+ )
self.assertRaisesRegex(
FeatureLibError,
"Unsupported contextual target sequence",
- self.parse, "feature test {"
- " ignore sub a' x x A';"
- "} test;")
+ self.parse,
+ "feature test {" " ignore sub a' x x A';" "} test;",
+ )
self.assertRaisesRegex(
FeatureLibError,
"Unsupported contextual target sequence",
- self.parse, "lookup upper {"
- " sub a by A;"
- "} upper;"
- "lookup lower {"
- " sub A by a;"
- "} lower;"
- "feature test {"
- " sub a' lookup upper x x A' lookup lower;"
- "} test;")
+ self.parse,
+ "lookup upper {"
+ " sub a by A;"
+ "} upper;"
+ "lookup lower {"
+ " sub A by a;"
+ "} lower;"
+ "feature test {"
+ " sub a' lookup upper x x A' lookup lower;"
+ "} test;",
+ )
def test_substitute_mix_single_multiple(self):
- doc = self.parse("lookup Look {"
- " sub f_f by f f;"
- " sub f by f;"
- " sub f_f_i by f f i;"
- " sub [a a.sc] by a;"
- " sub [a a.sc] by [b b.sc];"
- "} Look;")
+ doc = self.parse(
+ "lookup Look {"
+ " sub f_f by f f;"
+ " sub f by f;"
+ " sub f_f_i by f f i;"
+ " sub [a a.sc] by a;"
+ " sub [a a.sc] by [b b.sc];"
+ "} Look;"
+ )
statements = doc.statements[0].statements
for sub in statements:
self.assertIsInstance(sub, ast.MultipleSubstStatement)
@@ -1536,9 +1700,9 @@ class ParserTest(unittest.TestCase):
self.assertEqual(statements[6].replacement, ["b.sc"])
def test_substitute_from(self): # GSUB LookupType 3
- doc = self.parse("feature test {"
- " substitute a from [a.1 a.2 a.3];"
- "} test;")
+ doc = self.parse(
+ "feature test {" " substitute a from [a.1 a.2 a.3];" "} test;"
+ )
sub = doc.statements[0].statements[0]
self.assertIsInstance(sub, ast.AlternateSubstStatement)
self.assertEqual(glyphstr(sub.prefix), "")
@@ -1547,9 +1711,9 @@ class ParserTest(unittest.TestCase):
self.assertEqual(glyphstr([sub.replacement]), "[a.1 a.2 a.3]")
def test_substitute_from_chained(self): # chain to GSUB LookupType 3
- doc = self.parse("feature test {"
- " substitute A B a' [Y y] Z from [a.1 a.2 a.3];"
- "} test;")
+ doc = self.parse(
+ "feature test {" " substitute A B a' [Y y] Z from [a.1 a.2 a.3];" "} test;"
+ )
sub = doc.statements[0].statements[0]
self.assertIsInstance(sub, ast.AlternateSubstStatement)
self.assertEqual(glyphstr(sub.prefix), "A B")
@@ -1558,9 +1722,9 @@ class ParserTest(unittest.TestCase):
self.assertEqual(glyphstr([sub.replacement]), "[a.1 a.2 a.3]")
def test_substitute_from_cid(self): # GSUB LookupType 3
- doc = self.parse(r"feature test {"
- r" substitute \7 from [\111 \222];"
- r"} test;")
+ doc = self.parse(
+ r"feature test {" r" substitute \7 from [\111 \222];" r"} test;"
+ )
sub = doc.statements[0].statements[0]
self.assertIsInstance(sub, ast.AlternateSubstStatement)
self.assertEqual(glyphstr(sub.prefix), "")
@@ -1569,17 +1733,18 @@ class ParserTest(unittest.TestCase):
self.assertEqual(glyphstr([sub.replacement]), "[cid00111 cid00222]")
def test_substitute_from_glyphclass(self): # GSUB LookupType 3
- doc = self.parse("feature test {"
- " @Ampersands = [ampersand.1 ampersand.2];"
- " substitute ampersand from @Ampersands;"
- "} test;")
+ doc = self.parse(
+ "feature test {"
+ " @Ampersands = [ampersand.1 ampersand.2];"
+ " substitute ampersand from @Ampersands;"
+ "} test;"
+ )
[glyphclass, sub] = doc.statements[0].statements
self.assertIsInstance(sub, ast.AlternateSubstStatement)
self.assertEqual(glyphstr(sub.prefix), "")
self.assertEqual(glyphstr([sub.glyph]), "ampersand")
self.assertEqual(glyphstr(sub.suffix), "")
- self.assertEqual(glyphstr([sub.replacement]),
- "[ampersand.1 ampersand.2]")
+ self.assertEqual(glyphstr([sub.replacement]), "[ampersand.1 ampersand.2]")
def test_substitute_ligature(self): # GSUB LookupType 4
doc = self.parse("feature liga {substitute f f i by f_f_i;} liga;")
@@ -1609,13 +1774,15 @@ class ParserTest(unittest.TestCase):
self.assertRaisesRegex(
FeatureLibError,
'Expected "by", "from" or explicit lookup references',
- self.parse, "feature liga {substitute f f i;} liga;")
-
+ self.parse,
+ "feature liga {substitute f f i;} liga;",
+ )
+
def test_substitute_invalid_statement(self):
self.assertRaisesRegex(
FeatureLibError,
"Invalid substitution statement",
- Parser(self.getpath("GSUB_error.fea"), GLYPHNAMES).parse
+ Parser(self.getpath("GSUB_error.fea"), GLYPHNAMES).parse,
)
def test_subtable(self):
@@ -1625,8 +1792,11 @@ class ParserTest(unittest.TestCase):
def test_table_badEnd(self):
self.assertRaisesRegex(
- FeatureLibError, 'Expected "GDEF"', self.parse,
- "table GDEF {LigatureCaretByPos f_i 400;} ABCD;")
+ FeatureLibError,
+ 'Expected "GDEF"',
+ self.parse,
+ "table GDEF {LigatureCaretByPos f_i 400;} ABCD;",
+ )
def test_table_comment(self):
for table in "BASE GDEF OS/2 head hhea name vhea".split():
@@ -1637,8 +1807,11 @@ class ParserTest(unittest.TestCase):
def test_table_unsupported(self):
self.assertRaisesRegex(
- FeatureLibError, '"table Foo" is not supported', self.parse,
- "table Foo {LigatureCaretByPos f_i 400;} Foo;")
+ FeatureLibError,
+ '"table Foo" is not supported',
+ self.parse,
+ "table Foo {LigatureCaretByPos f_i 400;} Foo;",
+ )
def test_valuerecord_format_a_horizontal(self):
doc = self.parse("feature liga {valueRecordDef 123 foo;} liga;")
@@ -1702,12 +1875,13 @@ class ParserTest(unittest.TestCase):
def test_valuerecord_format_a_vertical_contexts_(self):
for tag in "vkrn vpal vhal valt".split():
- doc = self.parse(
- "feature %s {valueRecordDef 77 foo;} %s;" % (tag, tag))
+ doc = self.parse("feature %s {valueRecordDef 77 foo;} %s;" % (tag, tag))
value = doc.statements[0].statements[0].value
if value.yAdvance != 77:
- self.fail(msg="feature %s should be a vertical context "
- "for ValueRecord format A" % tag)
+ self.fail(
+ msg="feature %s should be a vertical context "
+ "for ValueRecord format A" % tag
+ )
def test_valuerecord_format_b(self):
doc = self.parse("feature liga {valueRecordDef <1 2 3 4> foo;} liga;")
@@ -1749,7 +1923,8 @@ class ParserTest(unittest.TestCase):
" <device NULL>"
" <device 33 -113, 44 -114, 55 115>"
" > foo;"
- "} liga;")
+ "} liga;"
+ )
value = doc.statements[0].statements[0].value
self.assertEqual(value.xPlacement, 1)
self.assertEqual(value.yPlacement, 2)
@@ -1759,9 +1934,11 @@ class ParserTest(unittest.TestCase):
self.assertEqual(value.yPlaDevice, ((11, 111), (12, 112)))
self.assertIsNone(value.xAdvDevice)
self.assertEqual(value.yAdvDevice, ((33, -113), (44, -114), (55, 115)))
- self.assertEqual(value.asFea(),
- "<1 2 3 4 <device 8 88> <device 11 111, 12 112>"
- " <device NULL> <device 33 -113, 44 -114, 55 115>>")
+ self.assertEqual(
+ value.asFea(),
+ "<1 2 3 4 <device 8 88> <device 11 111, 12 112>"
+ " <device NULL> <device 33 -113, 44 -114, 55 115>>",
+ )
def test_valuerecord_format_d(self):
doc = self.parse("feature test {valueRecordDef <NULL> foo;} test;")
@@ -1769,9 +1946,21 @@ class ParserTest(unittest.TestCase):
self.assertFalse(value)
self.assertEqual(value.asFea(), "<NULL>")
+ def test_valuerecord_variable_scalar(self):
+ doc = self.parse(
+ "feature test {valueRecordDef <0 (wght=200:-100 wght=900:-150 wdth=150,wght=900:-120) 0 0> foo;} test;"
+ )
+ value = doc.statements[0].statements[0].value
+ self.assertEqual(
+ value.asFea(),
+ "<0 (wght=200:-100 wght=900:-150 wdth=150,wght=900:-120) 0 0>",
+ )
+
def test_valuerecord_named(self):
- doc = self.parse("valueRecordDef <1 2 3 4> foo;"
- "feature liga {valueRecordDef <foo> bar;} liga;")
+ doc = self.parse(
+ "valueRecordDef <1 2 3 4> foo;"
+ "feature liga {valueRecordDef <foo> bar;} liga;"
+ )
value = doc.statements[1].statements[0].value
self.assertEqual(value.xPlacement, 1)
self.assertEqual(value.yPlacement, 2)
@@ -1780,8 +1969,11 @@ class ParserTest(unittest.TestCase):
def test_valuerecord_named_unknown(self):
self.assertRaisesRegex(
- FeatureLibError, "Unknown valueRecordDef \"unknown\"",
- self.parse, "valueRecordDef <unknown> foo;")
+ FeatureLibError,
+ 'Unknown valueRecordDef "unknown"',
+ self.parse,
+ "valueRecordDef <unknown> foo;",
+ )
def test_valuerecord_scoping(self):
[foo, liga, smcp] = self.parse(
@@ -1795,10 +1987,49 @@ class ParserTest(unittest.TestCase):
def test_valuerecord_device_value_out_of_range(self):
self.assertRaisesRegex(
- FeatureLibError, r"Device value out of valid range \(-128..127\)",
+ FeatureLibError,
+ r"Device value out of valid range \(-128..127\)",
self.parse,
"valueRecordDef <1 2 3 4 <device NULL> <device NULL> "
- "<device NULL> <device 11 128>> foo;")
+ "<device NULL> <device 11 128>> foo;",
+ )
+
+ def test_conditionset(self):
+ doc = self.parse("conditionset heavy { wght 700 900; } heavy;")
+ value = doc.statements[0]
+ self.assertEqual(value.conditions["wght"], (700, 900))
+ self.assertEqual(
+ value.asFea(), "conditionset heavy {\n wght 700 900;\n} heavy;\n"
+ )
+
+ doc = self.parse("conditionset heavy { wght 700 900; opsz 17 18;} heavy;")
+ value = doc.statements[0]
+ self.assertEqual(value.conditions["wght"], (700, 900))
+ self.assertEqual(value.conditions["opsz"], (17, 18))
+ self.assertEqual(
+ value.asFea(),
+ "conditionset heavy {\n wght 700 900;\n opsz 17 18;\n} heavy;\n",
+ )
+
+ def test_conditionset_same_axis(self):
+ self.assertRaisesRegex(
+ FeatureLibError,
+ r"Repeated condition for axis wght",
+ self.parse,
+ "conditionset heavy { wght 700 900; wght 100 200; } heavy;",
+ )
+
+ def test_conditionset_float(self):
+ doc = self.parse("conditionset heavy { wght 700.0 900.0; } heavy;")
+ value = doc.statements[0]
+ self.assertEqual(value.conditions["wght"], (700.0, 900.0))
+ self.assertEqual(
+ value.asFea(), "conditionset heavy {\n wght 700.0 900.0;\n} heavy;\n"
+ )
+
+ def test_variation(self):
+ doc = self.parse("variation rvrn heavy { sub a by b; } rvrn;")
+ value = doc.statements[0]
def test_languagesystem(self):
[langsys] = self.parse("languagesystem latn DEU;").statements
@@ -1810,20 +2041,30 @@ class ParserTest(unittest.TestCase):
self.assertRaisesRegex(
FeatureLibError,
'"dflt" is not a valid script tag; use "DFLT" instead',
- self.parse, "languagesystem dflt dflt;")
+ self.parse,
+ "languagesystem dflt dflt;",
+ )
self.assertRaisesRegex(
FeatureLibError,
'"DFLT" is not a valid language tag; use "dflt" instead',
- self.parse, "languagesystem latn DFLT;")
+ self.parse,
+ "languagesystem latn DFLT;",
+ )
self.assertRaisesRegex(
- FeatureLibError, "Expected ';'",
- self.parse, "languagesystem latn DEU")
+ FeatureLibError, "Expected ';'", self.parse, "languagesystem latn DEU"
+ )
self.assertRaisesRegex(
- FeatureLibError, "longer than 4 characters",
- self.parse, "languagesystem foobar DEU;")
+ FeatureLibError,
+ "longer than 4 characters",
+ self.parse,
+ "languagesystem foobar DEU;",
+ )
self.assertRaisesRegex(
- FeatureLibError, "longer than 4 characters",
- self.parse, "languagesystem latn FOOBAR;")
+ FeatureLibError,
+ "longer than 4 characters",
+ self.parse,
+ "languagesystem latn FOOBAR;",
+ )
def test_empty_statement_ignored(self):
doc = self.parse("feature test {;} test;")
@@ -1869,4 +2110,5 @@ class SymbolTableTest(unittest.TestCase):
if __name__ == "__main__":
import sys
+
sys.exit(unittest.main())