# -*- coding: utf-8 -*- from fontTools.misc.loggingTools import CapturingLogHandler from fontTools.feaLib.error import FeatureLibError from fontTools.feaLib.parser import Parser, SymbolTable from io import StringIO import warnings import fontTools.feaLib.ast as ast import os import unittest def glyphstr(glyphs): def f(x): if len(x) == 1: return list(x)[0] else: return "[%s]" % " ".join(sorted(list(x))) return " ".join(f(g.glyphSet()) for g in glyphs) def mapping(s): b = [] for a in s.glyphs: b.extend(a.glyphSet()) c = [] for a in s.replacements: c.extend(a.glyphSet()) if len(c) == 1: c = c * len(b) return dict(zip(b, c)) 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 A.swash B.swash X.swash Y.swash Z.swash 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 a.swash b.swash x.swash y.swash z.swash foobar foo.09 foo.1234 foo.9876 one two five six acute grave dieresis umlaut cedilla ogonek macron a_f_f_i o_f_f_i f_i f_l f_f_i one.fitted one.oldstyle a.1 a.2 a.3 c_t PRE SUF FIX BACK TRACK LOOK AHEAD ampersand ampersand.1 ampersand.2 cid00001 cid00002 cid00003 cid00004 cid00005 cid00006 cid00007 cid12345 cid78987 cid00999 cid01000 cid01001 cid00998 cid00995 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)] ) class ParserTest(unittest.TestCase): def __init__(self, methodName): unittest.TestCase.__init__(self, methodName) # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, # and fires deprecation warnings if a program uses the old name. if not hasattr(self, "assertRaisesRegex"): self.assertRaisesRegex = self.assertRaisesRegexp def test_glyphMap_deprecated(self): glyphMap = {"a": 0, "b": 1, "c": 2} with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") parser = Parser(StringIO(), glyphMap=glyphMap) 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.assertRaisesRegex( TypeError, "mutually exclusive", Parser, StringIO(), ("a",), glyphMap={"a": 0}, ) self.assertRaisesRegex( TypeError, "unsupported keyword argument", Parser, StringIO(), foo="bar" ) def test_comments(self): doc = self.parse( """ # Initial feature test { sub A by B; # simple } test;""" ) c1 = doc.statements[0] c2 = doc.statements[1].statements[1] self.assertEqual(type(c1), ast.Comment) self.assertEqual(c1.text, "# Initial") self.assertEqual(str(c1), "# Initial") self.assertEqual(type(c2), ast.Comment) self.assertEqual(c2.text, "# simple") self.assertEqual(doc.statements[1].name, "test") def test_only_comments(self): doc = self.parse( """\ # Initial """ ) c1 = doc.statements[0] self.assertEqual(type(c1), ast.Comment) self.assertEqual(c1.text, "# Initial") self.assertEqual(str(c1), "# Initial") def test_anchor_format_a(self): doc = self.parse( "feature test {" " pos cursive A ;" "} test;" ) anchor = doc.statements[0].statements[0].entryAnchor self.assertEqual(type(anchor), ast.Anchor) self.assertEqual(anchor.x, 120) self.assertEqual(anchor.y, -20) self.assertIsNone(anchor.contourpoint) self.assertIsNone(anchor.xDeviceTable) self.assertIsNone(anchor.yDeviceTable) def test_anchor_format_b(self): doc = self.parse( "feature test {" " pos cursive A ;" "} test;" ) anchor = doc.statements[0].statements[0].entryAnchor self.assertEqual(type(anchor), ast.Anchor) self.assertEqual(anchor.x, 120) self.assertEqual(anchor.y, -20) self.assertEqual(anchor.contourpoint, 5) self.assertIsNone(anchor.xDeviceTable) self.assertIsNone(anchor.yDeviceTable) def test_anchor_format_c(self): doc = self.parse( "feature test {" " pos cursive A " " >" " ;" "} test;" ) anchor = doc.statements[0].statements[0].entryAnchor self.assertEqual(type(anchor), ast.Anchor) self.assertEqual(anchor.x, 120) self.assertEqual(anchor.y, -20) self.assertIsNone(anchor.contourpoint) self.assertEqual(anchor.xDeviceTable, ((11, 111), (12, 112))) self.assertIsNone(anchor.yDeviceTable) def test_anchor_format_d(self): doc = self.parse( "feature test {" " pos cursive A ;" "} test;" ) anchor = doc.statements[0].statements[0].exitAnchor self.assertIsNone(anchor) def test_anchor_format_e(self): doc = self.parse( "feature test {" " anchorDef 120 -20 contourpoint 7 Foo;" " pos cursive A ;" "} test;" ) anchor = doc.statements[0].statements[1].entryAnchor self.assertEqual(type(anchor), ast.Anchor) self.assertEqual(anchor.x, 120) self.assertEqual(anchor.y, -20) self.assertEqual(anchor.contourpoint, 7) self.assertIsNone(anchor.xDeviceTable) self.assertIsNone(anchor.yDeviceTable) def test_anchor_format_e_undefined(self): self.assertRaisesRegex( FeatureLibError, 'Unknown anchor "UnknownName"', self.parse, "feature test {" " position cursive A ;" "} test;", ) def test_anchor_variable_scalar(self): doc = self.parse( "feature test {" " pos cursive A ;" "} test;" ) anchor = doc.statements[0].statements[0].entryAnchor self.assertEqual( anchor.asFea(), "", ) def test_anchordef(self): [foo] = self.parse("anchorDef 123 456 foo;").statements self.assertEqual(type(foo), ast.AnchorDefinition) self.assertEqual(foo.name, "foo") self.assertEqual(foo.x, 123) self.assertEqual(foo.y, 456) self.assertEqual(foo.contourpoint, None) def test_anchordef_contourpoint(self): [foo] = self.parse("anchorDef 123 456 contourpoint 5 foo;").statements self.assertEqual(type(foo), ast.AnchorDefinition) self.assertEqual(foo.name, "foo") self.assertEqual(foo.x, 123) self.assertEqual(foo.y, 456) self.assertEqual(foo.contourpoint, 5) def test_anon(self): anon = self.parse("anon TEST { # a\nfoo\n } TEST; # qux").statements[0] self.assertIsInstance(anon, ast.AnonymousBlock) self.assertEqual(anon.tag, "TEST") self.assertEqual(anon.content, "foo\n ") def test_anonymous(self): anon = self.parse("anonymous TEST {\nbar\n} TEST;").statements[0] self.assertIsInstance(anon, ast.AnonymousBlock) self.assertEqual(anon.tag, "TEST") # feature file spec requires passing the final end-of-line self.assertEqual(anon.content, "bar\n") def test_anon_missingBrace(self): self.assertRaisesRegex( 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;") s = doc.statements[0].statements[0] self.assertIsInstance(s, ast.AttachStatement) self.assertEqual(glyphstr([s.glyphs]), "[a e]") self.assertEqual(s.contourPoints, {2}) def test_feature_block(self): [liga] = self.parse("feature liga {} liga;").statements self.assertEqual(liga.name, "liga") self.assertFalse(liga.use_extension) def test_feature_block_useExtension(self): [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") def test_feature_comment(self): [liga] = self.parse("feature liga { # Comment\n } liga;").statements [comment] = liga.statements self.assertIsInstance(comment, ast.Comment) self.assertEqual(comment.text, "# Comment") def test_feature_reference(self): doc = self.parse("feature aalt { feature salt; } aalt;") ref = doc.statements[0].statements[0] self.assertIsInstance(ref, ast.FeatureReferenceStatement) self.assertEqual(ref.featureName, "salt") def test_FeatureNames_bad(self): self.assertRaisesRegex( 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 [featureNames] = feature.statements self.assertIsInstance(featureNames, ast.NestedBlock) [comment] = featureNames.statements self.assertIsInstance(comment, ast.Comment) self.assertEqual(comment.text, "# Comment") def test_FeatureNames_emptyStatements(self): [feature] = self.parse( "feature ss01 { featureNames { ;;; }; } ss01;" ).statements [featureNames] = feature.statements self.assertIsInstance(featureNames, ast.NestedBlock) self.assertEqual(featureNames.statements, []) def test_FontRevision(self): doc = self.parse("table head {FontRevision 2.5;} head;") s = doc.statements[0].statements[0] self.assertIsInstance(s, ast.FontRevisionStatement) self.assertEqual(s.revision, 2.5) def test_FontRevision_negative(self): self.assertRaisesRegex( 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")) with self.assertRaisesRegex( FeatureLibError, "(?s)missing from the glyph set:.*ccc" ): self.parse("@bad = [a b ccc];", glyphNames=("a", "b")) def test_glyphclass(self): [gc] = self.parse("@dash = [endash emdash figuredash];").statements self.assertEqual(gc.name, "dash") self.assertEqual(gc.glyphSet(), ("endash", "emdash", "figuredash")) def test_glyphclass_glyphNameTooLong(self): self.assertRaisesRegex( 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];", ) def test_glyphclass_duplicate(self): # makeotf accepts this, so we should too ab, xy = self.parse("@dup = [a b]; @dup = [x y];").statements self.assertEqual(glyphstr([ab]), "[a b]") self.assertEqual(glyphstr([xy]), "[x y]") def test_glyphclass_empty(self): [gc] = self.parse("@empty_set = [];").statements self.assertEqual(gc.name, "empty_set") self.assertEqual(gc.glyphSet(), tuple()) def test_glyphclass_equality(self): [foo, bar] = self.parse("@foo = [a b]; @bar = @foo;").statements self.assertEqual(foo.glyphSet(), ("a", "b")) self.assertEqual(bar.glyphSet(), ("a", "b")) def test_glyphclass_from_markClass(self): doc = self.parse( "markClass [acute grave] @TOP_MARKS;" "markClass cedilla @BOTTOM_MARKS;" "@MARKS = [@TOP_MARKS @BOTTOM_MARKS 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 self.assertEqual(gc.name, "GlyphClass") self.assertEqual(gc.glyphSet(), ("cid00999", "cid01000", "cid01001")) def test_glyphclass_range_cid_bad(self): self.assertRaisesRegex( FeatureLibError, "Bad range: start should be less than limit", self.parse, r"@bad = [\998-\995];", ) def test_glyphclass_range_uppercase(self): [gc] = self.parse("@swashes = [X.swash-Z.swash];").statements self.assertEqual(gc.name, "swashes") self.assertEqual(gc.glyphSet(), ("X.swash", "Y.swash", "Z.swash")) def test_glyphclass_range_lowercase(self): [gc] = self.parse("@defg.sc = [d.sc-g.sc];").statements self.assertEqual(gc.name, "defg.sc") self.assertEqual(gc.glyphSet(), ("d.sc", "e.sc", "f.sc", "g.sc")) def test_glyphclass_range_dash(self): glyphNames = "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", "B-foo.sc", "C-foo.sc")) def test_glyphclass_range_dash_with_space(self): gn = "A-foo.sc B-foo.sc C-foo.sc".split() [gc] = self.parse("@range = [A-foo.sc - C-foo.sc];", gn).statements self.assertEqual(gc.glyphSet(), ("A-foo.sc", "B-foo.sc", "C-foo.sc")) def test_glyphclass_ambiguous_dash_no_glyph_names(self): # If Parser is initialized without a glyphNames parameter (or with empty one) # it cannot distinguish between a glyph name containing an hyphen, or a # range of glyph names; thus it will interpret them as literal glyph names # while also outputting a logging warning to alert user about the ambiguity. # 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 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:") def test_glyphclass_glyph_name_should_win_over_range(self): # 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() [gc] = self.parse("@range = [A-foo.sc-C-foo.sc];", glyphNames).statements self.assertEqual(gc.glyphSet(), ("A-foo.sc-C-foo.sc",)) def test_glyphclass_range_dash_ambiguous(self): glyphNames = "A B C A-B B-C".split() self.assertRaisesRegex( 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, ) def test_glyphclass_range_digit1(self): [gc] = self.parse("@range = [foo.2-foo.5];").statements self.assertEqual(gc.glyphSet(), ("foo.2", "foo.3", "foo.4", "foo.5")) def test_glyphclass_range_digit2(self): [gc] = self.parse("@range = [foo.09-foo.11];").statements self.assertEqual(gc.glyphSet(), ("foo.09", "foo.10", "foo.11")) def test_glyphclass_range_digit3(self): [gc] = self.parse("@range = [foo.123-foo.125];").statements self.assertEqual(gc.glyphSet(), ("foo.123", "foo.124", "foo.125")) def test_glyphclass_range_bad(self): self.assertRaisesRegex( FeatureLibError, '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];", ) self.assertRaisesRegex( 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];", ) 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") ) 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 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.assertRaisesRegex( FeatureLibError, "Unknown glyph class @unknown", self.parse, "@bad = [@unknown];", ) def test_glyphclass_scoping(self): [foo, liga, smcp] = self.parse( "@foo = [a b];" "feature liga { @bar = [@foo l]; } liga;" "feature smcp { @bar = [@foo s]; } smcp;" ).statements self.assertEqual(foo.glyphSet(), ("a", "b")) self.assertEqual(liga.statements[0].glyphSet(), ("a", "b", "l")) self.assertEqual(smcp.statements[0].glyphSet(), ("a", "b", "s")) def test_glyphclass_scoping_bug496(self): # https://github.com/fonttools/fonttools/issues/496 f1, f2 = self.parse( "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"]) def test_GlyphClassDef(self): doc = self.parse("table GDEF {GlyphClassDef [b],[l],[m],[C c];} GDEF;") s = doc.statements[0].statements[0] self.assertIsInstance(s, ast.GlyphClassDefStatement) self.assertEqual(glyphstr([s.baseGlyphs]), "b") self.assertEqual(glyphstr([s.ligatureGlyphs]), "l") self.assertEqual(glyphstr([s.markGlyphs]), "m") self.assertEqual(glyphstr([s.componentGlyphs]), "[C c]") def test_GlyphClassDef_noCLassesSpecified(self): doc = self.parse("table GDEF {GlyphClassDef ,,,;} GDEF;") s = doc.statements[0].statements[0] self.assertIsNone(s.baseGlyphs) self.assertIsNone(s.ligatureGlyphs) self.assertIsNone(s.markGlyphs) self.assertIsNone(s.componentGlyphs) def test_ignore_pos(self): doc = self.parse("feature test {ignore pos e t' c, q u' u' x;} test;") sub = doc.statements[0].statements[0] self.assertIsInstance(sub, ast.IgnorePosStatement) [(pref1, glyphs1, suff1), (pref2, glyphs2, suff2)] = sub.chainContexts self.assertEqual(glyphstr(pref1), "e") self.assertEqual(glyphstr(glyphs1), "t") self.assertEqual(glyphstr(suff1), "c") self.assertEqual(glyphstr(pref2), "q") self.assertEqual(glyphstr(glyphs2), "u u") self.assertEqual(glyphstr(suff2), "x") def test_ignore_position(self): doc = self.parse( "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 self.assertEqual(glyphstr(prefix), "f [a e]") self.assertEqual(glyphstr(glyphs), "d [a u]") self.assertEqual(glyphstr(suffix), "[e y]") def test_ignore_position_with_lookup(self): self.assertRaisesRegex( FeatureLibError, '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;", ) def test_ignore_sub(self): doc = self.parse("feature test {ignore sub e t' c, q u' u' x;} test;") sub = doc.statements[0].statements[0] self.assertIsInstance(sub, ast.IgnoreSubstStatement) [(pref1, glyphs1, suff1), (pref2, glyphs2, suff2)] = sub.chainContexts self.assertEqual(glyphstr(pref1), "e") self.assertEqual(glyphstr(glyphs1), "t") self.assertEqual(glyphstr(suff1), "c") self.assertEqual(glyphstr(pref2), "q") self.assertEqual(glyphstr(glyphs2), "u u") self.assertEqual(glyphstr(suff2), "x") def test_ignore_substitute(self): doc = self.parse( "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 self.assertEqual(glyphstr(prefix), "f [a e]") self.assertEqual(glyphstr(glyphs), "d [a u]") self.assertEqual(glyphstr(suffix), "[e y]") def test_ignore_substitute_with_lookup(self): self.assertRaisesRegex( FeatureLibError, '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;", ) def test_include_statement(self): doc = self.parse( """\ include(../family.fea); include # Comment (foo) ; """, followIncludes=False, ) s1, s2, s3 = doc.statements self.assertEqual(type(s1), ast.IncludeStatement) self.assertEqual(s1.filename, "../family.fea") self.assertEqual(s1.asFea(), "include(../family.fea);") self.assertEqual(type(s2), ast.IncludeStatement) self.assertEqual(s2.filename, "foo") self.assertEqual(s2.asFea(), "include(foo);") self.assertEqual(type(s3), ast.Comment) self.assertEqual(s3.text, "# Comment") def test_include_statement_no_semicolon(self): doc = self.parse( """\ include(../family.fea) """, followIncludes=False, ) s1 = doc.statements[0] self.assertEqual(type(s1), ast.IncludeStatement) self.assertEqual(s1.filename, "../family.fea") self.assertEqual(s1.asFea(), "include(../family.fea);") def test_language(self): doc = self.parse("feature test {language DEU;} test;") s = doc.statements[0].statements[0] self.assertEqual(type(s), ast.LanguageStatement) self.assertEqual(s.language, "DEU ") self.assertTrue(s.include_default) self.assertFalse(s.required) def test_language_exclude_dflt(self): doc = self.parse("feature test {language DEU exclude_dflt;} test;") s = doc.statements[0].statements[0] self.assertEqual(type(s), ast.LanguageStatement) self.assertEqual(s.language, "DEU ") self.assertFalse(s.include_default) self.assertFalse(s.required) def test_language_exclude_dflt_required(self): 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 ") self.assertFalse(s.include_default) self.assertTrue(s.required) def test_language_include_dflt(self): doc = self.parse("feature test {language DEU include_dflt;} test;") s = doc.statements[0].statements[0] self.assertEqual(type(s), ast.LanguageStatement) self.assertEqual(s.language, "DEU ") self.assertTrue(s.include_default) self.assertFalse(s.required) def test_language_include_dflt_required(self): 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 ") self.assertTrue(s.include_default) self.assertTrue(s.required) def test_language_DFLT(self): self.assertRaisesRegex( FeatureLibError, '"DFLT" is not a valid language tag; use "dflt" instead', self.parse, "feature test { language DFLT; } test;", ) def test_ligatureCaretByIndex_glyphClass(self): doc = self.parse("table GDEF{LigatureCaretByIndex [c_t f_i] 2;}GDEF;") s = doc.statements[0].statements[0] self.assertIsInstance(s, ast.LigatureCaretByIndexStatement) self.assertEqual(glyphstr([s.glyphs]), "[c_t f_i]") self.assertEqual(s.carets, [2]) def test_ligatureCaretByIndex_singleGlyph(self): doc = self.parse("table GDEF{LigatureCaretByIndex f_f_i 3 7;}GDEF;") s = doc.statements[0].statements[0] self.assertIsInstance(s, ast.LigatureCaretByIndexStatement) self.assertEqual(glyphstr([s.glyphs]), "f_f_i") self.assertEqual(s.carets, [3, 7]) def test_ligatureCaretByPos_glyphClass(self): doc = self.parse("table GDEF {LigatureCaretByPos [c_t f_i] 7;} GDEF;") s = doc.statements[0].statements[0] self.assertIsInstance(s, ast.LigatureCaretByPosStatement) self.assertEqual(glyphstr([s.glyphs]), "[c_t f_i]") self.assertEqual(s.carets, [7]) def test_ligatureCaretByPos_singleGlyph(self): doc = self.parse("table GDEF {LigatureCaretByPos f_i 400 380;} GDEF;") s = doc.statements[0].statements[0] self.assertIsInstance(s, ast.LigatureCaretByPosStatement) self.assertEqual(glyphstr([s.glyphs]), "f_i") self.assertEqual(s.carets, [400, 380]) def test_ligatureCaretByPos_variable_scalar(self): doc = self.parse( "table GDEF {LigatureCaretByPos f_i (wght=200:400 wght=900:1000) 380;} GDEF;" ) s = doc.statements[0].statements[0] self.assertIsInstance(s, ast.LigatureCaretByPosStatement) self.assertEqual(glyphstr([s.glyphs]), "f_i") self.assertEqual(len(s.carets), 2) self.assertEqual(str(s.carets[0]), "(wght=200:400 wght=900:1000)") self.assertEqual(s.carets[1], 380) def test_lookup_block(self): [lookup] = self.parse("lookup Ligatures {} Ligatures;").statements self.assertEqual(lookup.name, "Ligatures") self.assertFalse(lookup.use_extension) def test_lookup_block_useExtension(self): [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") def test_lookup_block_name_mismatch(self): self.assertRaisesRegex( 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;" ) [liga] = doc.statements [look] = liga.statements [foo] = look.statements self.assertEqual(foo.value.xAdvance, 123) 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;" ) [vkrn] = doc.statements [look] = vkrn.statements [foo] = look.statements self.assertIsNone(foo.value.xAdvance) self.assertEqual(foo.value.yAdvance, 123) def test_lookup_comment(self): [lookup] = self.parse("lookup L { # Comment\n } L;").statements [comment] = lookup.statements self.assertIsInstance(comment, ast.Comment) self.assertEqual(comment.text, "# Comment") def test_lookup_reference(self): [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 [foo] = qux.statements [ref] = bar.statements self.assertIsInstance(ref, ast.LookupReferenceStatement) self.assertEqual(ref.lookup, foo) def test_lookup_reference_unknown(self): self.assertRaisesRegex( 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] def test_lookupflag_format_A(self): flag = self.parse_lookupflag_("lookupflag RightToLeft IgnoreMarks;") self.assertIsInstance(flag, ast.LookupFlagStatement) self.assertEqual(flag.value, 9) self.assertIsNone(flag.markAttachment) self.assertIsNone(flag.markFilteringSet) self.assertEqual(flag.asFea(), "lookupflag RightToLeft IgnoreMarks;") def test_lookupflag_format_A_MarkAttachmentType(self): flag = self.parse_lookupflag_( "@TOP_MARKS = [acute grave macron];" "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.assertIsNone(flag.markFilteringSet) 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];" ) 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.assertIsNone(flag.markFilteringSet) 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;" ) 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;", ) def test_lookupflag_format_A_UseMarkFilteringSet_glyphClass(self): flag = self.parse_lookupflag_( "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];", ) def test_lookupflag_format_B(self): flag = self.parse_lookupflag_("lookupflag 7;") self.assertIsInstance(flag, ast.LookupFlagStatement) self.assertEqual(flag.value, 7) self.assertIsNone(flag.markAttachment) self.assertIsNone(flag.markFilteringSet) self.assertEqual( flag.asFea(), "lookupflag RightToLeft IgnoreBaseGlyphs IgnoreLigatures;" ) def test_lookupflag_format_B_zero(self): flag = self.parse_lookupflag_("lookupflag 0;") self.assertIsInstance(flag, ast.LookupFlagStatement) self.assertEqual(flag.value, 0) self.assertIsNone(flag.markAttachment) self.assertIsNone(flag.markFilteringSet) self.assertEqual(flag.asFea(), "lookupflag 0;") def test_lookupflag_no_value(self): self.assertRaisesRegex( FeatureLibError, "lookupflag must have a value", self.parse, "feature test {lookupflag;} test;", ) def test_lookupflag_repeated(self): self.assertRaisesRegex( FeatureLibError, "RightToLeft can be specified only once", self.parse, "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;", ) def test_gpos_type_1_glyph(self): doc = self.parse("feature kern {pos one <1 2 3 4>;} kern;") pos = doc.statements[0].statements[0] self.assertIsInstance(pos, ast.SinglePosStatement) [(glyphs, value)] = pos.pos self.assertEqual(glyphstr([glyphs]), "one") self.assertEqual(value.asFea(), "<1 2 3 4>") def test_gpos_type_1_glyphclass_horizontal(self): doc = self.parse("feature kern {pos [one two] -300;} kern;") pos = doc.statements[0].statements[0] self.assertIsInstance(pos, ast.SinglePosStatement) [(glyphs, value)] = pos.pos self.assertEqual(glyphstr([glyphs]), "[one two]") self.assertEqual(value.asFea(), "-300") def test_gpos_type_1_glyphclass_vertical(self): doc = self.parse("feature vkrn {pos [one two] -300;} vkrn;") pos = doc.statements[0].statements[0] self.assertIsInstance(pos, ast.SinglePosStatement) [(glyphs, value)] = pos.pos self.assertEqual(glyphstr([glyphs]), "[one two]") self.assertEqual(value.asFea(), "-300") def test_gpos_type_1_multiple(self): doc = self.parse("feature f {pos one'1 two'2 [five six]'56;} f;") pos = doc.statements[0].statements[0] self.assertIsInstance(pos, ast.SinglePosStatement) [(glyphs1, val1), (glyphs2, val2), (glyphs3, val3)] = pos.pos self.assertEqual(glyphstr([glyphs1]), "one") self.assertEqual(val1.asFea(), "1") self.assertEqual(glyphstr([glyphs2]), "two") self.assertEqual(val2.asFea(), "2") self.assertEqual(glyphstr([glyphs3]), "[five six]") self.assertEqual(val3.asFea(), "56") self.assertEqual(pos.prefix, []) self.assertEqual(pos.suffix, []) def test_gpos_type_1_enumerated(self): self.assertRaisesRegex( FeatureLibError, '"enumerate" is only allowed with pair positionings', 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;", ) def test_gpos_type_1_chained(self): doc = self.parse("feature kern {pos [A B] [T Y]' 20 comma;} 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(), "20") self.assertEqual(glyphstr(pos.prefix), "[A B]") self.assertEqual(glyphstr(pos.suffix), "comma") def test_gpos_type_1_chained_special_kern_format_valuerecord_format_a(self): doc = self.parse("feature kern {pos [A B] [T Y]' comma 20;} 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(), "20") 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(self): doc = self.parse("feature kern {pos [A B] [T Y]' comma <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") 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;" ) pos = doc.statements[0].statements[0] self.assertEqual(type(pos), ast.PairPosStatement) self.assertFalse(pos.enumerated) self.assertEqual(glyphstr([pos.glyphs1]), "[T V]") self.assertEqual(pos.valuerecord1.asFea(), "-60") self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]") 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;" ) pos = doc.statements[0].statements[0] self.assertEqual(type(pos), ast.PairPosStatement) self.assertTrue(pos.enumerated) self.assertEqual(glyphstr([pos.glyphs1]), "[T V]") self.assertEqual(pos.valuerecord1.asFea(), "-60") self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]") 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] [a b c] <1 2 3 4>;" "} kern;" ) pos = doc.statements[0].statements[0] self.assertEqual(type(pos), ast.PairPosStatement) self.assertFalse(pos.enumerated) self.assertEqual(glyphstr([pos.glyphs1]), "[T V]") self.assertFalse(pos.valuerecord1) self.assertEqual(pos.valuerecord1.asFea(), "") self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]") self.assertEqual(pos.valuerecord2.asFea(), "<1 2 3 4>") self.assertEqual(pos.asFea(), "pos [T V] [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] ;" "} kern;" ) pos = doc.statements[0].statements[0] self.assertEqual(type(pos), ast.PairPosStatement) self.assertFalse(pos.enumerated) self.assertEqual(glyphstr([pos.glyphs1]), "[T V]") self.assertEqual(pos.valuerecord1.asFea(), "<1 2 3 4>") self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]") self.assertFalse(pos.valuerecord2) 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;") pos = doc.statements[0].statements[0] self.assertEqual(type(pos), ast.PairPosStatement) self.assertFalse(pos.enumerated) self.assertEqual(glyphstr([pos.glyphs1]), "[T V]") self.assertEqual(pos.valuerecord1.asFea(), "<1 2 3 4>") self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]") 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;" ) pos = doc.statements[0].statements[0] self.assertEqual(type(pos), ast.PairPosStatement) self.assertTrue(pos.enumerated) self.assertEqual(glyphstr([pos.glyphs1]), "[T V]") self.assertEqual(pos.valuerecord1.asFea(), "<1 2 3 4>") self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]") self.assertIsNone(pos.valuerecord2) def test_gpos_type_3(self): doc = self.parse( "feature kern {" " position cursive A ;" "} kern;" ) pos = doc.statements[0].statements[0] self.assertEqual(type(pos), ast.CursivePosStatement) self.assertEqual(pos.glyphclass.glyphSet(), ("A",)) self.assertEqual((pos.entryAnchor.x, pos.entryAnchor.y), (12, -2)) self.assertEqual((pos.exitAnchor.x, pos.exitAnchor.y), (2, 3)) def test_gpos_type_3_enumerated(self): self.assertRaisesRegex( FeatureLibError, '"enumerate" is not allowed with cursive attachment positioning', self.parse, "feature kern {" " enumerate position cursive A ;" "} kern;", ) def test_gpos_type_4(self): doc = self.parse( "markClass [acute grave] @TOP_MARKS;" "markClass [dieresis umlaut] @TOP_MARKS;" "markClass [cedilla] @BOTTOM_MARKS;" "feature test {" " position base [a e o u] " " mark @TOP_MARKS " " mark @BOTTOM_MARKS;" "} test;" ) pos = doc.statements[-1].statements[0] self.assertEqual(type(pos), ast.MarkBasePosStatement) self.assertEqual(pos.base.glyphSet(), ("a", "e", "o", "u")) (a1, m1), (a2, m2) = pos.marks self.assertEqual((a1.x, a1.y, m1.name), (250, 450, "TOP_MARKS")) self.assertEqual((a2.x, a2.y, m2.name), (210, -10, "BOTTOM_MARKS")) def test_gpos_type_4_enumerated(self): self.assertRaisesRegex( FeatureLibError, '"enumerate" is not allowed with ' "mark-to-base attachment positioning", self.parse, "feature kern {" " markClass cedilla @BOTTOM_MARKS;" " enumerate position base A mark @BOTTOM_MARKS;" "} kern;", ) def test_gpos_type_4_not_markClass(self): self.assertRaisesRegex( FeatureLibError, "@MARKS is not a markClass", self.parse, "@MARKS = [acute grave];" "feature test {" " position base [a e o u] mark @MARKS;" "} test;", ) def test_gpos_type_5(self): doc = self.parse( "markClass [grave acute] @TOP_MARKS;" "markClass [cedilla] @BOTTOM_MARKS;" "feature test {" " position " " ligature [a_f_f_i o_f_f_i] " " mark @TOP_MARKS " " mark @BOTTOM_MARKS " " ligComponent " " mark @TOP_MARKS " " ligComponent " " " " ligComponent " " mark @BOTTOM_MARKS;" "} 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")) [(a11, m11), (a12, m12)], [(a2, m2)], [], [(a4, m4)] = pos.marks self.assertEqual((a11.x, a11.y, m11.name), (50, 600, "TOP_MARKS")) self.assertEqual((a12.x, a12.y, m12.name), (50, -10, "BOTTOM_MARKS")) self.assertEqual((a2.x, a2.y, m2.name), (30, 800, "TOP_MARKS")) self.assertEqual((a4.x, a4.y, m4.name), (30, -10, "BOTTOM_MARKS")) def test_gpos_type_5_enumerated(self): self.assertRaisesRegex( FeatureLibError, '"enumerate" is not allowed with ' "mark-to-ligature attachment positioning", self.parse, "feature test {" " markClass cedilla @MARKS;" " enumerate position " " ligature f_i mark @MARKS" " ligComponent ;" "} test;", ) def test_gpos_type_5_not_markClass(self): self.assertRaisesRegex( FeatureLibError, "@MARKS is not a markClass", self.parse, "@MARKS = [acute grave];" "feature test {" " position ligature f_i mark @MARKS;" "} test;", ) def test_gpos_type_6(self): doc = self.parse( "markClass damma @MARK_CLASS_1;" "feature test {" " position mark hamza mark @MARK_CLASS_1;" "} test;" ) pos = doc.statements[-1].statements[0] self.assertEqual(type(pos), ast.MarkMarkPosStatement) self.assertEqual(pos.baseMarks.glyphSet(), ("hamza",)) [(a1, m1)] = pos.marks self.assertEqual((a1.x, a1.y, m1.name), (221, 301, "MARK_CLASS_1")) def test_gpos_type_6_enumerated(self): self.assertRaisesRegex( FeatureLibError, '"enumerate" is not allowed with ' "mark-to-mark attachment positioning", self.parse, "markClass damma @MARK_CLASS_1;" "feature test {" " enum pos mark hamza mark @MARK_CLASS_1;" "} test;", ) def test_gpos_type_6_not_markClass(self): self.assertRaisesRegex( FeatureLibError, "@MARKS is not a markClass", self.parse, "@MARKS = [acute grave];" "feature test {" " position mark cedilla mark @MARKS;" "} 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;" ) lookup1, lookup2 = doc.statements[0:2] pos = doc.statements[-1].statements[0] self.assertEqual(type(pos), ast.ChainContextPosStatement) self.assertEqual(glyphstr(pos.prefix), "[A a] [B b]") self.assertEqual(glyphstr(pos.glyphs), "I [N n] P") self.assertEqual(glyphstr(pos.suffix), "[Y y] [Z z]") self.assertEqual(pos.lookups, [[lookup1], [lookup2], None]) def test_gpos_type_8_lookup_with_values(self): self.assertRaisesRegex( FeatureLibError, 'If "lookup" is present, no values must be specified', self.parse, "lookup L1 {pos one 100;} L1;" "feature test {" " pos A' lookup L1 B' 20;" "} test;", ) def test_markClass(self): doc = self.parse("markClass [acute grave] @MARKS;") mc = doc.statements[0] self.assertIsInstance(mc, ast.MarkClassDefinition) self.assertEqual(mc.markClass.name, "MARKS") self.assertEqual(mc.glyphSet(), ("acute", "grave")) 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;') name = doc.statements[0].statements[0] self.assertIsInstance(name, ast.NameRecord) self.assertEqual(name.nameID, 9) self.assertEqual(name.platformID, 3) self.assertEqual(name.platEncID, 1) self.assertEqual(name.langID, 0x0409) self.assertEqual(name.string, "Müller-Lancé") self.assertEqual(name.asFea(), r'nameid 9 "M\00fcller-Lanc\00e9";') def test_nameid_windows_utf16_backslash(self): doc = self.parse(r'table name { nameid 9 "Back\005cslash"; } name;') name = doc.statements[0].statements[0] self.assertEqual(name.string, r"Back\slash") 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;') name = doc.statements[0].statements[0] self.assertEqual(name.string, 'Quotation "Mark"') self.assertEqual(name.asFea(), r'nameid 9 "Quotation \0022Mark\0022";') def test_nameid_windows_utf16_surroates(self): doc = self.parse(r'table name { nameid 9 "Carrot \D83E\DD55"; } name;') name = doc.statements[0].statements[0] self.assertEqual(name.string, r"Carrot 🥕") 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;') name = doc.statements[0].statements[0] self.assertIsInstance(name, ast.NameRecord) self.assertEqual(name.nameID, 9) self.assertEqual(name.platformID, 1) 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";') def test_nameid_mac_croatian(self): 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) self.assertEqual(name.platEncID, 0) self.assertEqual(name.langID, 18) self.assertEqual(name.string, "Jovica Veljović") self.assertEqual(name.asFea(), r'nameid 9 1 0 18 "Jovica Veljovi\e6";') def test_nameid_unsupported_platform(self): self.assertRaisesRegex( 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;') name = doc.statements[0].statements[0] self.assertEqual(name.nameID, 9) self.assertEqual(name.platformID, 3) self.assertEqual(name.platEncID, 1) self.assertEqual(name.langID, 0x0409) def test_nameid_octal(self): 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) self.assertEqual(name.platEncID, 10) self.assertEqual(name.langID, 0o2011) def test_cv_hexadecimal(self): 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;") cv = doc.statements[0].statements[0].statements[0] self.assertEqual(cv.character, 0o56736) def test_rsub_format_a(self): doc = self.parse("feature test {rsub a [b B] c' d [e E] by C;} test;") rsub = doc.statements[0].statements[0] self.assertEqual(type(rsub), ast.ReverseChainSingleSubstStatement) self.assertEqual(glyphstr(rsub.old_prefix), "a [B b]") self.assertEqual(rsub.glyphs[0].glyphSet(), ("c",)) self.assertEqual(rsub.replacements[0].glyphSet(), ("C",)) self.assertEqual(glyphstr(rsub.old_suffix), "d [E e]") def test_rsub_format_a_cid(self): 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(rsub.glyphs[0].glyphSet(), ("cid00004",)) self.assertEqual(rsub.replacements[0].glyphSet(), ("cid00006",)) self.assertEqual(glyphstr(rsub.old_suffix), "cid00005") def test_rsub_format_b(self): doc = self.parse( "feature smcp {" " reversesub A B [one.fitted one.oldstyle]' C [d D] by one;" "} 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"}) def test_rsub_format_c(self): doc = self.parse( "feature test {" " reversesub BACK TRACK [a-d]' LOOK AHEAD by [A.sc-D.sc];" "} 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"} ) 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;", ) 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;", ) def test_rsub_multiple_replacement_glyphs(self): self.assertRaisesRegex( FeatureLibError, "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;", ) def test_script(self): doc = self.parse("feature test {script cyrl;} test;") s = doc.statements[0].statements[0] self.assertEqual(type(s), ast.ScriptStatement) self.assertEqual(s.script, "cyrl") def test_script_dflt(self): self.assertRaisesRegex( FeatureLibError, '"dflt" is not a valid script tag; use "DFLT" instead', 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;' ) da = doc.statements[0].statements[0] self.assertIsInstance(da, ast.STATDesignAxisStatement) self.assertEqual(da.tag, "opsz") self.assertEqual(da.axisOrder, 0) 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;" ) avr = doc.statements[0].statements[1] self.assertIsInstance(avr, ast.STATAxisValueStatement) self.assertEqual(avr.locations[0].tag, "opsz") self.assertEqual(avr.locations[0].values[0], 8) 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;" ) avr = doc.statements[0].statements[1] self.assertIsInstance(avr, ast.STATAxisValueStatement) self.assertEqual(avr.locations[0].tag, "opsz") self.assertEqual(avr.locations[0].values, [8, 6, 10]) 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;", ) 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;", ) def test_stat_elidedfallbackname(self): # STAT ElidedFallbackName 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, "ローマン") def test_stat_elidedfallbacknameid(self): # STAT ElidedFallbackNameID 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") def test_sub_single_format_a(self): # GSUB LookupType 1 doc = self.parse("feature smcp {substitute a by a.sc;} smcp;") sub = doc.statements[0].statements[0] self.assertIsInstance(sub, ast.SingleSubstStatement) self.assertEqual(glyphstr(sub.prefix), "") self.assertEqual(mapping(sub), {"a": "a.sc"}) self.assertEqual(glyphstr(sub.suffix), "") def test_sub_single_format_a_chained(self): # chain to GSUB LookupType 1 doc = self.parse("feature test {sub [A a] d' [C] by d.alt;} test;") sub = doc.statements[0].statements[0] self.assertIsInstance(sub, ast.SingleSubstStatement) self.assertEqual(mapping(sub), {"d": "d.alt"}) self.assertEqual(glyphstr(sub.prefix), "[A a]") self.assertEqual(glyphstr(sub.suffix), "C") def test_sub_single_format_a_cid(self): # GSUB LookupType 1 doc = self.parse(r"feature smcp {substitute \12345 by \78987;} smcp;") sub = doc.statements[0].statements[0] self.assertIsInstance(sub, ast.SingleSubstStatement) self.assertEqual(glyphstr(sub.prefix), "") self.assertEqual(mapping(sub), {"cid12345": "cid78987"}) self.assertEqual(glyphstr(sub.suffix), "") def test_sub_single_format_b(self): # GSUB LookupType 1 doc = self.parse( "feature smcp {" " substitute [one.fitted one.oldstyle] by one;" "} smcp;" ) sub = doc.statements[0].statements[0] self.assertIsInstance(sub, ast.SingleSubstStatement) self.assertEqual(mapping(sub), {"one.fitted": "one", "one.oldstyle": "one"}) self.assertEqual(glyphstr(sub.prefix), "") self.assertEqual(glyphstr(sub.suffix), "") def test_sub_single_format_b_chained(self): # chain to GSUB LookupType 1 doc = self.parse( "feature smcp {" " substitute PRE FIX [one.fitted one.oldstyle]' SUF FIX by one;" "} smcp;" ) sub = doc.statements[0].statements[0] self.assertIsInstance(sub, ast.SingleSubstStatement) 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;" ) 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(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;" ) 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(glyphstr(sub.prefix), "") self.assertEqual(glyphstr(sub.suffix), "X Y [Z z]") def test_sub_single_format_c_different_num_elements(self): 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;", ) 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;", ) def test_substitute_multiple(self): # GSUB LookupType 2 doc = self.parse("lookup Look {substitute f_f_i by f f i;} Look;") sub = doc.statements[0].statements[0] self.assertIsInstance(sub, ast.MultipleSubstStatement) self.assertEqual(glyphstr([sub.glyph]), "f_f_i") self.assertEqual(glyphstr(sub.replacement), "f f i") def test_substitute_multiple_chained(self): # chain to GSUB LookupType 2 doc = self.parse("lookup L {sub [A-C] f_f_i' [X-Z] by f f i;} L;") sub = doc.statements[0].statements[0] self.assertIsInstance(sub, ast.MultipleSubstStatement) self.assertEqual(glyphstr([sub.glyph]), "f_f_i") self.assertEqual(glyphstr(sub.replacement), "f f i") def test_substitute_multiple_force_chained(self): doc = self.parse("lookup L {sub f_f_i' by f f i;} L;") sub = doc.statements[0].statements[0] self.assertIsInstance(sub, ast.MultipleSubstStatement) self.assertEqual(glyphstr([sub.glyph]), "f_f_i") self.assertEqual(glyphstr(sub.replacement), "f f i") self.assertEqual(sub.asFea(), "sub f_f_i' by f f i;") def test_substitute_multiple_classes(self): doc = self.parse("lookup Look {substitute [f_i f_l] by [f f] [i l];} Look;") sub = doc.statements[0].statements[0] self.assertIsInstance(sub, ast.MultipleSubstStatement) self.assertEqual(glyphstr([sub.glyph]), "[f_i f_l]") self.assertEqual(glyphstr(sub.replacement), "[f f] [i l]") def test_substitute_multiple_classes_mixed(self): doc = self.parse("lookup Look {substitute [f_i f_l] by f [i l];} Look;") sub = doc.statements[0].statements[0] self.assertIsInstance(sub, ast.MultipleSubstStatement) self.assertEqual(glyphstr([sub.glyph]), "[f_i f_l]") self.assertEqual(glyphstr(sub.replacement), "f [i l]") def test_substitute_multiple_classes_mixed_singleton(self): doc = self.parse("lookup Look {substitute [f_i f_l] by [f] [i l];} Look;") sub = doc.statements[0].statements[0] self.assertIsInstance(sub, ast.MultipleSubstStatement) self.assertEqual(glyphstr([sub.glyph]), "[f_i f_l]") self.assertEqual(glyphstr(sub.replacement), "f [i l]") def test_substitute_multiple_classes_mismatch(self): self.assertRaisesRegex( FeatureLibError, 'Expected a glyph class with 1 or 3 elements after "by", ' "but found a glyph class with 2 elements", self.parse, "lookup Look {substitute [f_i f_l f_f_i] by [f f_f] [i l i];} Look;", ) def test_substitute_multiple_by_mutliple(self): self.assertRaisesRegex( FeatureLibError, "Direct substitution of multiple glyphs by multiple glyphs " "is not supported", self.parse, "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.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.assertRaisesRegex( FeatureLibError, "Unsupported contextual target sequence", 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;", ) 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;" ) statements = doc.statements[0].statements for sub in statements: self.assertIsInstance(sub, ast.MultipleSubstStatement) self.assertEqual(statements[1].glyph, "f") self.assertEqual(statements[1].replacement, ["f"]) self.assertEqual(statements[3].glyph, "a") self.assertEqual(statements[3].replacement, ["a"]) self.assertEqual(statements[4].glyph, "a.sc") self.assertEqual(statements[4].replacement, ["a"]) self.assertEqual(statements[5].glyph, "a") self.assertEqual(statements[5].replacement, ["b"]) self.assertEqual(statements[6].glyph, "a.sc") 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;" ) sub = doc.statements[0].statements[0] self.assertIsInstance(sub, ast.AlternateSubstStatement) self.assertEqual(glyphstr(sub.prefix), "") self.assertEqual(glyphstr([sub.glyph]), "a") self.assertEqual(glyphstr(sub.suffix), "") 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;" ) sub = doc.statements[0].statements[0] self.assertIsInstance(sub, ast.AlternateSubstStatement) self.assertEqual(glyphstr(sub.prefix), "A B") self.assertEqual(glyphstr([sub.glyph]), "a") self.assertEqual(glyphstr(sub.suffix), "[Y y] Z") 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;" ) sub = doc.statements[0].statements[0] self.assertIsInstance(sub, ast.AlternateSubstStatement) self.assertEqual(glyphstr(sub.prefix), "") self.assertEqual(glyphstr([sub.glyph]), "cid00007") self.assertEqual(glyphstr(sub.suffix), "") 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;" ) [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]") def test_substitute_ligature(self): # GSUB LookupType 4 doc = self.parse("feature liga {substitute f f i by f_f_i;} liga;") sub = doc.statements[0].statements[0] self.assertIsInstance(sub, ast.LigatureSubstStatement) self.assertEqual(glyphstr(sub.glyphs), "f f i") self.assertEqual(sub.replacement, "f_f_i") self.assertEqual(glyphstr(sub.prefix), "") self.assertEqual(glyphstr(sub.suffix), "") def test_substitute_ligature_chained(self): # chain to GSUB LookupType 4 doc = self.parse("feature F {substitute A B f' i' Z by f_i;} F;") sub = doc.statements[0].statements[0] self.assertIsInstance(sub, ast.LigatureSubstStatement) self.assertEqual(glyphstr(sub.glyphs), "f i") self.assertEqual(sub.replacement, "f_i") self.assertEqual(glyphstr(sub.prefix), "A B") self.assertEqual(glyphstr(sub.suffix), "Z") def test_substitute_lookups(self): # GSUB LookupType 6 doc = Parser(self.getpath("spec5fi1.fea"), GLYPHNAMES).parse() [_, _, _, langsys, ligs, sub, feature] = doc.statements self.assertEqual(feature.statements[0].lookups, [[ligs], None, [sub]]) self.assertEqual(feature.statements[1].lookups, [[ligs], None, [sub]]) def test_substitute_missing_by(self): self.assertRaisesRegex( FeatureLibError, 'Expected "by", "from" or explicit lookup references', 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, ) def test_subtable(self): doc = self.parse("feature test {subtable;} test;") s = doc.statements[0].statements[0] self.assertIsInstance(s, ast.SubtableStatement) def test_table_badEnd(self): self.assertRaisesRegex( 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(): doc = self.parse("table %s { # Comment\n } %s;" % (table, table)) comment = doc.statements[0].statements[0] self.assertIsInstance(comment, ast.Comment) self.assertEqual(comment.text, "# Comment") def test_table_unsupported(self): self.assertRaisesRegex( 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;") valuedef = doc.statements[0].statements[0] value = valuedef.value self.assertIsNone(value.xPlacement) self.assertIsNone(value.yPlacement) self.assertEqual(value.xAdvance, 123) self.assertIsNone(value.yAdvance) self.assertIsNone(value.xPlaDevice) self.assertIsNone(value.yPlaDevice) self.assertIsNone(value.xAdvDevice) self.assertIsNone(value.yAdvDevice) self.assertEqual(valuedef.asFea(), "valueRecordDef 123 foo;") self.assertEqual(value.asFea(), "123") def test_valuerecord_format_a_vertical(self): doc = self.parse("feature vkrn {valueRecordDef 123 foo;} vkrn;") valuedef = doc.statements[0].statements[0] value = valuedef.value self.assertIsNone(value.xPlacement) self.assertIsNone(value.yPlacement) self.assertIsNone(value.xAdvance) self.assertEqual(value.yAdvance, 123) self.assertIsNone(value.xPlaDevice) self.assertIsNone(value.yPlaDevice) self.assertIsNone(value.xAdvDevice) self.assertIsNone(value.yAdvDevice) self.assertEqual(valuedef.asFea(), "valueRecordDef 123 foo;") self.assertEqual(value.asFea(), "123") def test_valuerecord_format_a_zero_horizontal(self): doc = self.parse("feature liga {valueRecordDef 0 foo;} liga;") valuedef = doc.statements[0].statements[0] value = valuedef.value self.assertIsNone(value.xPlacement) self.assertIsNone(value.yPlacement) self.assertEqual(value.xAdvance, 0) self.assertIsNone(value.yAdvance) self.assertIsNone(value.xPlaDevice) self.assertIsNone(value.yPlaDevice) self.assertIsNone(value.xAdvDevice) self.assertIsNone(value.yAdvDevice) self.assertEqual(valuedef.asFea(), "valueRecordDef 0 foo;") self.assertEqual(value.asFea(), "0") def test_valuerecord_format_a_zero_vertical(self): doc = self.parse("feature vkrn {valueRecordDef 0 foo;} vkrn;") valuedef = doc.statements[0].statements[0] value = valuedef.value self.assertIsNone(value.xPlacement) self.assertIsNone(value.yPlacement) self.assertIsNone(value.xAdvance) self.assertEqual(value.yAdvance, 0) self.assertIsNone(value.xPlaDevice) self.assertIsNone(value.yPlaDevice) self.assertIsNone(value.xAdvDevice) self.assertIsNone(value.yAdvDevice) self.assertEqual(valuedef.asFea(), "valueRecordDef 0 foo;") self.assertEqual(value.asFea(), "0") 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)) 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 ) def test_valuerecord_format_b(self): doc = self.parse("feature liga {valueRecordDef <1 2 3 4> foo;} liga;") valuedef = doc.statements[0].statements[0] value = valuedef.value self.assertEqual(value.xPlacement, 1) self.assertEqual(value.yPlacement, 2) self.assertEqual(value.xAdvance, 3) self.assertEqual(value.yAdvance, 4) self.assertIsNone(value.xPlaDevice) self.assertIsNone(value.yPlaDevice) self.assertIsNone(value.xAdvDevice) self.assertIsNone(value.yAdvDevice) self.assertEqual(valuedef.asFea(), "valueRecordDef <1 2 3 4> foo;") self.assertEqual(value.asFea(), "<1 2 3 4>") def test_valuerecord_format_b_zero(self): doc = self.parse("feature liga {valueRecordDef <0 0 0 0> foo;} liga;") valuedef = doc.statements[0].statements[0] value = valuedef.value self.assertEqual(value.xPlacement, 0) self.assertEqual(value.yPlacement, 0) self.assertEqual(value.xAdvance, 0) self.assertEqual(value.yAdvance, 0) self.assertIsNone(value.xPlaDevice) self.assertIsNone(value.yPlaDevice) self.assertIsNone(value.xAdvDevice) self.assertIsNone(value.yAdvDevice) self.assertEqual(valuedef.asFea(), "valueRecordDef <0 0 0 0> foo;") self.assertEqual(value.asFea(), "<0 0 0 0>") def test_valuerecord_format_c(self): doc = self.parse( "feature liga {" " valueRecordDef <" " 1 2 3 4" " " " " " " " " " > foo;" "} liga;" ) value = doc.statements[0].statements[0].value self.assertEqual(value.xPlacement, 1) self.assertEqual(value.yPlacement, 2) self.assertEqual(value.xAdvance, 3) self.assertEqual(value.yAdvance, 4) self.assertEqual(value.xPlaDevice, ((8, 88),)) 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 " " >", ) def test_valuerecord_format_d(self): doc = self.parse("feature test {valueRecordDef foo;} test;") value = doc.statements[0].statements[0].value self.assertFalse(value) self.assertEqual(value.asFea(), "") 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 bar;} liga;" ) value = doc.statements[1].statements[0].value self.assertEqual(value.xPlacement, 1) self.assertEqual(value.yPlacement, 2) self.assertEqual(value.xAdvance, 3) self.assertEqual(value.yAdvance, 4) def test_valuerecord_named_unknown(self): self.assertRaisesRegex( FeatureLibError, 'Unknown valueRecordDef "unknown"', self.parse, "valueRecordDef foo;", ) def test_valuerecord_scoping(self): [foo, liga, smcp] = self.parse( "valueRecordDef 789 foo;" "feature liga {valueRecordDef bar;} liga;" "feature smcp {valueRecordDef bar;} smcp;" ).statements self.assertEqual(foo.value.xAdvance, 789) self.assertEqual(liga.statements[0].value.xAdvance, 789) self.assertEqual(smcp.statements[0].value.xAdvance, 789) def test_valuerecord_device_value_out_of_range(self): self.assertRaisesRegex( FeatureLibError, r"Device value out of valid range \(-128..127\)", self.parse, "valueRecordDef <1 2 3 4 " " > 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 self.assertEqual(langsys.script, "latn") self.assertEqual(langsys.language, "DEU ") [langsys] = self.parse("languagesystem DFLT DEU;").statements self.assertEqual(langsys.script, "DFLT") self.assertEqual(langsys.language, "DEU ") self.assertRaisesRegex( FeatureLibError, '"dflt" is not a valid script tag; use "DFLT" instead', self.parse, "languagesystem dflt dflt;", ) self.assertRaisesRegex( FeatureLibError, '"DFLT" is not a valid language tag; use "dflt" instead', self.parse, "languagesystem latn DFLT;", ) self.assertRaisesRegex( FeatureLibError, "Expected ';'", self.parse, "languagesystem latn DEU" ) self.assertRaisesRegex( FeatureLibError, "longer than 4 characters", self.parse, "languagesystem foobar DEU;", ) self.assertRaisesRegex( FeatureLibError, "longer than 4 characters", self.parse, "languagesystem latn FOOBAR;", ) def test_empty_statement_ignored(self): doc = self.parse("feature test {;} test;") self.assertFalse(doc.statements[0].statements) doc = self.parse(";;;") self.assertFalse(doc.statements) for table in "BASE GDEF OS/2 head hhea name vhea".split(): doc = self.parse("table %s { ;;; } %s;" % (table, table)) self.assertEqual(doc.statements[0].statements, []) def test_ufo_features_parse_include_dir(self): fea_path = self.getpath("include/test.ufo/features.fea") include_dir = os.path.dirname(os.path.dirname(fea_path)) doc = Parser(fea_path, includeDir=include_dir).parse() assert len(doc.statements) == 1 and doc.statements[0].text == "# Nothing" def test_unmarked_ignore_statement(self): with CapturingLogHandler("fontTools.feaLib.parser", level="WARNING") as caplog: doc = self.parse("lookup foo { ignore sub A; } foo;") self.assertEqual(doc.statements[0].statements[0].asFea(), "ignore sub A';") self.assertEqual(len(caplog.records), 1) caplog.assertRegex( 'Ambiguous "ignore sub", there should be least one marked glyph' ) def parse(self, text, glyphNames=GLYPHNAMES, followIncludes=True): featurefile = StringIO(text) p = Parser(featurefile, glyphNames, followIncludes=followIncludes) return p.parse() @staticmethod def getpath(testfile): path, _ = os.path.split(__file__) return os.path.join(path, "data", testfile) class SymbolTableTest(unittest.TestCase): def test_scopes(self): symtab = SymbolTable() symtab.define("foo", 23) self.assertEqual(symtab.resolve("foo"), 23) symtab.enter_scope() self.assertEqual(symtab.resolve("foo"), 23) symtab.define("foo", 42) self.assertEqual(symtab.resolve("foo"), 42) symtab.exit_scope() self.assertEqual(symtab.resolve("foo"), 23) def test_resolve_undefined(self): self.assertEqual(SymbolTable().resolve("abc"), None) if __name__ == "__main__": import sys sys.exit(unittest.main())