| # -*- 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_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 <anchor 120 -20> <anchor NULL>;" |
| "} 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 <anchor 120 -20 contourpoint 5> <anchor NULL>;" |
| "} 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 " |
| " <anchor 120 -20 <device 11 111, 12 112> <device NULL>>" |
| " <anchor NULL>;" |
| "} 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 <anchor 120 -20> <anchor NULL>;" |
| "} 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 <anchor Foo> <anchor NULL>;" |
| "} 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 <anchor UnknownName> <anchor NULL>;" |
| "} test;") |
| |
| 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, "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] <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")) |
| |
| 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_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_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] <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) |
| self.assertEqual(glyphstr([pos.glyphs1]), "[T V]") |
| self.assertFalse(pos.valuerecord1) |
| self.assertEqual(pos.valuerecord1.asFea(), "<NULL>") |
| self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]") |
| self.assertEqual(pos.valuerecord2.asFea(), "<1 2 3 4>") |
| 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;") |
| 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 <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",)) |
| 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 <anchor 12 -2> <anchor 2 3>;" |
| "} kern;") |
| |
| def test_gpos_type_4(self): |
| doc = self.parse( |
| "markClass [acute grave] <anchor 150 -10> @TOP_MARKS;" |
| "markClass [dieresis umlaut] <anchor 300 -10> @TOP_MARKS;" |
| "markClass [cedilla] <anchor 300 600> @BOTTOM_MARKS;" |
| "feature test {" |
| " position base [a e o u] " |
| " <anchor 250 450> mark @TOP_MARKS " |
| " <anchor 210 -10> 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 <anchor 300 600> @BOTTOM_MARKS;" |
| " enumerate position base A <anchor 12 -2> 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] <anchor 250 450> mark @MARKS;" |
| "} test;") |
| |
| def test_gpos_type_5(self): |
| doc = self.parse( |
| "markClass [grave acute] <anchor 150 500> @TOP_MARKS;" |
| "markClass [cedilla] <anchor 300 -100> @BOTTOM_MARKS;" |
| "feature test {" |
| " position " |
| " ligature [a_f_f_i o_f_f_i] " |
| " <anchor 50 600> mark @TOP_MARKS " |
| " <anchor 50 -10> mark @BOTTOM_MARKS " |
| " ligComponent " |
| " <anchor 30 800> mark @TOP_MARKS " |
| " ligComponent " |
| " <anchor NULL> " |
| " ligComponent " |
| " <anchor 30 -10> 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 <anchor 300 600> @MARKS;" |
| " enumerate position " |
| " ligature f_i <anchor 100 0> mark @MARKS" |
| " ligComponent <anchor NULL>;" |
| "} 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 <anchor 250 450> mark @MARKS;" |
| "} 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;") |
| 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 <anchor 189 -103> @MARK_CLASS_1;" |
| "feature test {" |
| " enum pos mark hamza <anchor 221 301> 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 <anchor 250 450> 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] <anchor 350 3> @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(sub.glyph, "f_f_i") |
| self.assertEqual(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(sub.glyph, "f_f_i") |
| self.assertEqual(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(sub.glyph, "f_f_i") |
| self.assertEqual(sub.replacement, ("f", "f", "i")) |
| self.assertEqual(sub.asFea(), "sub f_f_i' by f f i;") |
| |
| 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" |
| " <device 8 88>" |
| " <device 11 111, 12 112>" |
| " <device NULL>" |
| " <device 33 -113, 44 -114, 55 115>" |
| " > 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 <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;") |
| value = doc.statements[0].statements[0].value |
| self.assertFalse(value) |
| self.assertEqual(value.asFea(), "<NULL>") |
| |
| def test_valuerecord_named(self): |
| 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) |
| 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 <unknown> foo;") |
| |
| def test_valuerecord_scoping(self): |
| [foo, liga, smcp] = self.parse( |
| "valueRecordDef 789 foo;" |
| "feature liga {valueRecordDef <foo> bar;} liga;" |
| "feature smcp {valueRecordDef <foo> 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 <device NULL> <device NULL> " |
| "<device NULL> <device 11 128>> foo;") |
| |
| 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 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()) |