xref: /aosp_15_r20/external/fonttools/Tests/feaLib/builder_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughesfrom fontTools.misc.loggingTools import CapturingLogHandler
2*e1fe3e4aSElliott Hughesfrom fontTools.feaLib.builder import (
3*e1fe3e4aSElliott Hughes    Builder,
4*e1fe3e4aSElliott Hughes    addOpenTypeFeatures,
5*e1fe3e4aSElliott Hughes    addOpenTypeFeaturesFromString,
6*e1fe3e4aSElliott Hughes)
7*e1fe3e4aSElliott Hughesfrom fontTools.feaLib.error import FeatureLibError
8*e1fe3e4aSElliott Hughesfrom fontTools.ttLib import TTFont, newTable
9*e1fe3e4aSElliott Hughesfrom fontTools.feaLib.parser import Parser
10*e1fe3e4aSElliott Hughesfrom fontTools.feaLib import ast
11*e1fe3e4aSElliott Hughesfrom fontTools.feaLib.lexer import Lexer
12*e1fe3e4aSElliott Hughesfrom fontTools.fontBuilder import addFvar
13*e1fe3e4aSElliott Hughesimport difflib
14*e1fe3e4aSElliott Hughesfrom io import StringIO
15*e1fe3e4aSElliott Hughesimport os
16*e1fe3e4aSElliott Hughesimport re
17*e1fe3e4aSElliott Hughesimport shutil
18*e1fe3e4aSElliott Hughesimport sys
19*e1fe3e4aSElliott Hughesimport tempfile
20*e1fe3e4aSElliott Hughesimport logging
21*e1fe3e4aSElliott Hughesimport unittest
22*e1fe3e4aSElliott Hughesimport warnings
23*e1fe3e4aSElliott Hughes
24*e1fe3e4aSElliott Hughes
25*e1fe3e4aSElliott Hughesdef makeTTFont():
26*e1fe3e4aSElliott Hughes    glyphs = """
27*e1fe3e4aSElliott Hughes        .notdef space slash fraction semicolon period comma ampersand
28*e1fe3e4aSElliott Hughes        quotedblleft quotedblright quoteleft quoteright
29*e1fe3e4aSElliott Hughes        zero one two three four five six seven eight nine
30*e1fe3e4aSElliott Hughes        zero.oldstyle one.oldstyle two.oldstyle three.oldstyle
31*e1fe3e4aSElliott Hughes        four.oldstyle five.oldstyle six.oldstyle seven.oldstyle
32*e1fe3e4aSElliott Hughes        eight.oldstyle nine.oldstyle onequarter onehalf threequarters
33*e1fe3e4aSElliott Hughes        onesuperior twosuperior threesuperior ordfeminine ordmasculine
34*e1fe3e4aSElliott Hughes        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
35*e1fe3e4aSElliott Hughes        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
36*e1fe3e4aSElliott Hughes        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
37*e1fe3e4aSElliott Hughes        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
38*e1fe3e4aSElliott Hughes        A.alt1 A.alt2 A.alt3 B.alt1 B.alt2 B.alt3 C.alt1 C.alt2 C.alt3
39*e1fe3e4aSElliott Hughes        a.alt1 a.alt2 a.alt3 a.end b.alt c.mid d.alt d.mid
40*e1fe3e4aSElliott Hughes        e.begin e.mid e.end m.begin n.end s.end z.end
41*e1fe3e4aSElliott Hughes        Eng Eng.alt1 Eng.alt2 Eng.alt3
42*e1fe3e4aSElliott Hughes        A.swash B.swash C.swash D.swash E.swash F.swash G.swash H.swash
43*e1fe3e4aSElliott Hughes        I.swash J.swash K.swash L.swash M.swash N.swash O.swash P.swash
44*e1fe3e4aSElliott Hughes        Q.swash R.swash S.swash T.swash U.swash V.swash W.swash X.swash
45*e1fe3e4aSElliott Hughes        Y.swash Z.swash
46*e1fe3e4aSElliott Hughes        f_l c_h c_k c_s c_t f_f f_f_i f_f_l f_i o_f_f_i s_t f_i.begin
47*e1fe3e4aSElliott Hughes        a_n_d T_h T_h.swash germandbls ydieresis yacute breve
48*e1fe3e4aSElliott Hughes        grave acute dieresis macron circumflex cedilla umlaut ogonek caron
49*e1fe3e4aSElliott Hughes        damma hamza sukun kasratan lam_meem_jeem noon.final noon.initial
50*e1fe3e4aSElliott Hughes        by feature lookup sub table uni0327 uni0328 e.fina
51*e1fe3e4aSElliott Hughes    """.split()
52*e1fe3e4aSElliott Hughes    glyphs.extend("cid{:05d}".format(cid) for cid in range(800, 1001 + 1))
53*e1fe3e4aSElliott Hughes    font = TTFont()
54*e1fe3e4aSElliott Hughes    font.setGlyphOrder(glyphs)
55*e1fe3e4aSElliott Hughes    return font
56*e1fe3e4aSElliott Hughes
57*e1fe3e4aSElliott Hughes
58*e1fe3e4aSElliott Hughesclass BuilderTest(unittest.TestCase):
59*e1fe3e4aSElliott Hughes    # Feature files in data/*.fea; output gets compared to data/*.ttx.
60*e1fe3e4aSElliott Hughes    TEST_FEATURE_FILES = """
61*e1fe3e4aSElliott Hughes        Attach cid_range enum markClass language_required
62*e1fe3e4aSElliott Hughes        GlyphClassDef LigatureCaretByIndex LigatureCaretByPos
63*e1fe3e4aSElliott Hughes        lookup lookupflag feature_aalt ignore_pos
64*e1fe3e4aSElliott Hughes        GPOS_1 GPOS_1_zero GPOS_2 GPOS_2b GPOS_3 GPOS_4 GPOS_5 GPOS_6 GPOS_8
65*e1fe3e4aSElliott Hughes        GSUB_2 GSUB_3 GSUB_6 GSUB_8
66*e1fe3e4aSElliott Hughes        spec4h1 spec4h2 spec5d1 spec5d2 spec5fi1 spec5fi2 spec5fi3 spec5fi4
67*e1fe3e4aSElliott Hughes        spec5f_ii_1 spec5f_ii_2 spec5f_ii_3 spec5f_ii_4
68*e1fe3e4aSElliott Hughes        spec5h1 spec6b_ii spec6d2 spec6e spec6f
69*e1fe3e4aSElliott Hughes        spec6h_ii spec6h_iii_1 spec6h_iii_3d spec8a spec8b spec8c spec8d
70*e1fe3e4aSElliott Hughes        spec9a spec9b spec9c1 spec9c2 spec9c3 spec9d spec9e spec9f spec9g
71*e1fe3e4aSElliott Hughes        spec10
72*e1fe3e4aSElliott Hughes        bug453 bug457 bug463 bug501 bug502 bug504 bug505 bug506 bug509
73*e1fe3e4aSElliott Hughes        bug512 bug514 bug568 bug633 bug1307 bug1459 bug2276 variable_bug2772
74*e1fe3e4aSElliott Hughes        name size size2 multiple_feature_blocks omitted_GlyphClassDef
75*e1fe3e4aSElliott Hughes        ZeroValue_SinglePos_horizontal ZeroValue_SinglePos_vertical
76*e1fe3e4aSElliott Hughes        ZeroValue_PairPos_horizontal ZeroValue_PairPos_vertical
77*e1fe3e4aSElliott Hughes        ZeroValue_ChainSinglePos_horizontal ZeroValue_ChainSinglePos_vertical
78*e1fe3e4aSElliott Hughes        PairPosSubtable ChainSubstSubtable SubstSubtable ChainPosSubtable
79*e1fe3e4aSElliott Hughes        LigatureSubtable AlternateSubtable MultipleSubstSubtable
80*e1fe3e4aSElliott Hughes        SingleSubstSubtable aalt_chain_contextual_subst AlternateChained
81*e1fe3e4aSElliott Hughes        MultipleLookupsPerGlyph MultipleLookupsPerGlyph2 GSUB_6_formats
82*e1fe3e4aSElliott Hughes        GSUB_5_formats delete_glyph STAT_test STAT_test_elidedFallbackNameID
83*e1fe3e4aSElliott Hughes        variable_scalar_valuerecord variable_scalar_anchor variable_conditionset
84*e1fe3e4aSElliott Hughes        variable_mark_anchor
85*e1fe3e4aSElliott Hughes    """.split()
86*e1fe3e4aSElliott Hughes
87*e1fe3e4aSElliott Hughes    VARFONT_AXES = [
88*e1fe3e4aSElliott Hughes        ("wght", 200, 200, 1000, "Weight"),
89*e1fe3e4aSElliott Hughes        ("wdth", 100, 100, 200, "Width"),
90*e1fe3e4aSElliott Hughes    ]
91*e1fe3e4aSElliott Hughes
92*e1fe3e4aSElliott Hughes    def __init__(self, methodName):
93*e1fe3e4aSElliott Hughes        unittest.TestCase.__init__(self, methodName)
94*e1fe3e4aSElliott Hughes        # Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
95*e1fe3e4aSElliott Hughes        # and fires deprecation warnings if a program uses the old name.
96*e1fe3e4aSElliott Hughes        if not hasattr(self, "assertRaisesRegex"):
97*e1fe3e4aSElliott Hughes            self.assertRaisesRegex = self.assertRaisesRegexp
98*e1fe3e4aSElliott Hughes
99*e1fe3e4aSElliott Hughes    def setUp(self):
100*e1fe3e4aSElliott Hughes        self.tempdir = None
101*e1fe3e4aSElliott Hughes        self.num_tempfiles = 0
102*e1fe3e4aSElliott Hughes
103*e1fe3e4aSElliott Hughes    def tearDown(self):
104*e1fe3e4aSElliott Hughes        if self.tempdir:
105*e1fe3e4aSElliott Hughes            shutil.rmtree(self.tempdir)
106*e1fe3e4aSElliott Hughes
107*e1fe3e4aSElliott Hughes    @staticmethod
108*e1fe3e4aSElliott Hughes    def getpath(testfile):
109*e1fe3e4aSElliott Hughes        path, _ = os.path.split(__file__)
110*e1fe3e4aSElliott Hughes        return os.path.join(path, "data", testfile)
111*e1fe3e4aSElliott Hughes
112*e1fe3e4aSElliott Hughes    def temp_path(self, suffix):
113*e1fe3e4aSElliott Hughes        if not self.tempdir:
114*e1fe3e4aSElliott Hughes            self.tempdir = tempfile.mkdtemp()
115*e1fe3e4aSElliott Hughes        self.num_tempfiles += 1
116*e1fe3e4aSElliott Hughes        return os.path.join(self.tempdir, "tmp%d%s" % (self.num_tempfiles, suffix))
117*e1fe3e4aSElliott Hughes
118*e1fe3e4aSElliott Hughes    def read_ttx(self, path):
119*e1fe3e4aSElliott Hughes        lines = []
120*e1fe3e4aSElliott Hughes        with open(path, "r", encoding="utf-8") as ttx:
121*e1fe3e4aSElliott Hughes            for line in ttx.readlines():
122*e1fe3e4aSElliott Hughes                # Elide ttFont attributes because ttLibVersion may change.
123*e1fe3e4aSElliott Hughes                if line.startswith("<ttFont "):
124*e1fe3e4aSElliott Hughes                    lines.append("<ttFont>\n")
125*e1fe3e4aSElliott Hughes                else:
126*e1fe3e4aSElliott Hughes                    lines.append(line.rstrip() + "\n")
127*e1fe3e4aSElliott Hughes        return lines
128*e1fe3e4aSElliott Hughes
129*e1fe3e4aSElliott Hughes    def expect_ttx(self, font, expected_ttx, replace=None):
130*e1fe3e4aSElliott Hughes        path = self.temp_path(suffix=".ttx")
131*e1fe3e4aSElliott Hughes        font.saveXML(
132*e1fe3e4aSElliott Hughes            path,
133*e1fe3e4aSElliott Hughes            tables=[
134*e1fe3e4aSElliott Hughes                "head",
135*e1fe3e4aSElliott Hughes                "name",
136*e1fe3e4aSElliott Hughes                "BASE",
137*e1fe3e4aSElliott Hughes                "GDEF",
138*e1fe3e4aSElliott Hughes                "GSUB",
139*e1fe3e4aSElliott Hughes                "GPOS",
140*e1fe3e4aSElliott Hughes                "OS/2",
141*e1fe3e4aSElliott Hughes                "STAT",
142*e1fe3e4aSElliott Hughes                "hhea",
143*e1fe3e4aSElliott Hughes                "vhea",
144*e1fe3e4aSElliott Hughes            ],
145*e1fe3e4aSElliott Hughes        )
146*e1fe3e4aSElliott Hughes        actual = self.read_ttx(path)
147*e1fe3e4aSElliott Hughes        expected = self.read_ttx(expected_ttx)
148*e1fe3e4aSElliott Hughes        if replace:
149*e1fe3e4aSElliott Hughes            for i in range(len(expected)):
150*e1fe3e4aSElliott Hughes                for k, v in replace.items():
151*e1fe3e4aSElliott Hughes                    expected[i] = expected[i].replace(k, v)
152*e1fe3e4aSElliott Hughes        if actual != expected:
153*e1fe3e4aSElliott Hughes            for line in difflib.unified_diff(
154*e1fe3e4aSElliott Hughes                expected, actual, fromfile=expected_ttx, tofile=path
155*e1fe3e4aSElliott Hughes            ):
156*e1fe3e4aSElliott Hughes                sys.stderr.write(line)
157*e1fe3e4aSElliott Hughes            self.fail("TTX output is different from expected")
158*e1fe3e4aSElliott Hughes
159*e1fe3e4aSElliott Hughes    def build(self, featureFile, tables=None):
160*e1fe3e4aSElliott Hughes        font = makeTTFont()
161*e1fe3e4aSElliott Hughes        addOpenTypeFeaturesFromString(font, featureFile, tables=tables)
162*e1fe3e4aSElliott Hughes        return font
163*e1fe3e4aSElliott Hughes
164*e1fe3e4aSElliott Hughes    def check_feature_file(self, name):
165*e1fe3e4aSElliott Hughes        font = makeTTFont()
166*e1fe3e4aSElliott Hughes        if name.startswith("variable_"):
167*e1fe3e4aSElliott Hughes            font["name"] = newTable("name")
168*e1fe3e4aSElliott Hughes            addFvar(font, self.VARFONT_AXES, [])
169*e1fe3e4aSElliott Hughes            del font["name"]
170*e1fe3e4aSElliott Hughes        feapath = self.getpath("%s.fea" % name)
171*e1fe3e4aSElliott Hughes        addOpenTypeFeatures(font, feapath)
172*e1fe3e4aSElliott Hughes        self.expect_ttx(font, self.getpath("%s.ttx" % name))
173*e1fe3e4aSElliott Hughes        # Check that:
174*e1fe3e4aSElliott Hughes        # 1) tables do compile (only G* tables as long as we have a mock font)
175*e1fe3e4aSElliott Hughes        # 2) dumping after save-reload yields the same TTX dump as before
176*e1fe3e4aSElliott Hughes        for tag in ("GDEF", "GSUB", "GPOS"):
177*e1fe3e4aSElliott Hughes            if tag in font:
178*e1fe3e4aSElliott Hughes                data = font[tag].compile(font)
179*e1fe3e4aSElliott Hughes                font[tag].decompile(data, font)
180*e1fe3e4aSElliott Hughes        self.expect_ttx(font, self.getpath("%s.ttx" % name))
181*e1fe3e4aSElliott Hughes        # Optionally check a debug dump.
182*e1fe3e4aSElliott Hughes        debugttx = self.getpath("%s-debug.ttx" % name)
183*e1fe3e4aSElliott Hughes        if os.path.exists(debugttx):
184*e1fe3e4aSElliott Hughes            addOpenTypeFeatures(font, feapath, debug=True)
185*e1fe3e4aSElliott Hughes            self.expect_ttx(font, debugttx, replace={"__PATH__": feapath})
186*e1fe3e4aSElliott Hughes
187*e1fe3e4aSElliott Hughes    def check_fea2fea_file(self, name, base=None, parser=Parser):
188*e1fe3e4aSElliott Hughes        font = makeTTFont()
189*e1fe3e4aSElliott Hughes        fname = (name + ".fea") if "." not in name else name
190*e1fe3e4aSElliott Hughes        p = parser(self.getpath(fname), glyphNames=font.getGlyphOrder())
191*e1fe3e4aSElliott Hughes        doc = p.parse()
192*e1fe3e4aSElliott Hughes        actual = self.normal_fea(doc.asFea().split("\n"))
193*e1fe3e4aSElliott Hughes        with open(self.getpath(base or fname), "r", encoding="utf-8") as ofile:
194*e1fe3e4aSElliott Hughes            expected = self.normal_fea(ofile.readlines())
195*e1fe3e4aSElliott Hughes
196*e1fe3e4aSElliott Hughes        if expected != actual:
197*e1fe3e4aSElliott Hughes            fname = name.rsplit(".", 1)[0] + ".fea"
198*e1fe3e4aSElliott Hughes            for line in difflib.unified_diff(
199*e1fe3e4aSElliott Hughes                expected,
200*e1fe3e4aSElliott Hughes                actual,
201*e1fe3e4aSElliott Hughes                fromfile=fname + " (expected)",
202*e1fe3e4aSElliott Hughes                tofile=fname + " (actual)",
203*e1fe3e4aSElliott Hughes            ):
204*e1fe3e4aSElliott Hughes                sys.stderr.write(line + "\n")
205*e1fe3e4aSElliott Hughes            self.fail(
206*e1fe3e4aSElliott Hughes                "Fea2Fea output is different from expected. "
207*e1fe3e4aSElliott Hughes                "Generated:\n{}\n".format("\n".join(actual))
208*e1fe3e4aSElliott Hughes            )
209*e1fe3e4aSElliott Hughes
210*e1fe3e4aSElliott Hughes    def normal_fea(self, lines):
211*e1fe3e4aSElliott Hughes        output = []
212*e1fe3e4aSElliott Hughes        skip = 0
213*e1fe3e4aSElliott Hughes        for l in lines:
214*e1fe3e4aSElliott Hughes            l = l.strip()
215*e1fe3e4aSElliott Hughes            if l.startswith("#test-fea2fea:"):
216*e1fe3e4aSElliott Hughes                if len(l) > 15:
217*e1fe3e4aSElliott Hughes                    output.append(l[15:].strip())
218*e1fe3e4aSElliott Hughes                skip = 1
219*e1fe3e4aSElliott Hughes            x = l.find("#")
220*e1fe3e4aSElliott Hughes            if x >= 0:
221*e1fe3e4aSElliott Hughes                l = l[:x].strip()
222*e1fe3e4aSElliott Hughes            if not len(l):
223*e1fe3e4aSElliott Hughes                continue
224*e1fe3e4aSElliott Hughes            if skip > 0:
225*e1fe3e4aSElliott Hughes                skip = skip - 1
226*e1fe3e4aSElliott Hughes                continue
227*e1fe3e4aSElliott Hughes            output.append(l)
228*e1fe3e4aSElliott Hughes        return output
229*e1fe3e4aSElliott Hughes
230*e1fe3e4aSElliott Hughes    def make_mock_vf(self):
231*e1fe3e4aSElliott Hughes        font = makeTTFont()
232*e1fe3e4aSElliott Hughes        font["name"] = newTable("name")
233*e1fe3e4aSElliott Hughes        addFvar(font, self.VARFONT_AXES, [])
234*e1fe3e4aSElliott Hughes        del font["name"]
235*e1fe3e4aSElliott Hughes        return font
236*e1fe3e4aSElliott Hughes
237*e1fe3e4aSElliott Hughes    @staticmethod
238*e1fe3e4aSElliott Hughes    def get_region(var_region_axis):
239*e1fe3e4aSElliott Hughes        return (
240*e1fe3e4aSElliott Hughes            var_region_axis.StartCoord,
241*e1fe3e4aSElliott Hughes            var_region_axis.PeakCoord,
242*e1fe3e4aSElliott Hughes            var_region_axis.EndCoord,
243*e1fe3e4aSElliott Hughes        )
244*e1fe3e4aSElliott Hughes
245*e1fe3e4aSElliott Hughes    def test_alternateSubst_multipleSubstitutionsForSameGlyph(self):
246*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
247*e1fe3e4aSElliott Hughes            FeatureLibError,
248*e1fe3e4aSElliott Hughes            'Already defined alternates for glyph "A"',
249*e1fe3e4aSElliott Hughes            self.build,
250*e1fe3e4aSElliott Hughes            "feature test {"
251*e1fe3e4aSElliott Hughes            "    sub A from [A.alt1 A.alt2];"
252*e1fe3e4aSElliott Hughes            "    sub B from [B.alt1 B.alt2 B.alt3];"
253*e1fe3e4aSElliott Hughes            "    sub A from [A.alt1 A.alt2];"
254*e1fe3e4aSElliott Hughes            "} test;",
255*e1fe3e4aSElliott Hughes        )
256*e1fe3e4aSElliott Hughes
257*e1fe3e4aSElliott Hughes    def test_singleSubst_multipleIdenticalSubstitutionsForSameGlyph_info(self):
258*e1fe3e4aSElliott Hughes        logger = logging.getLogger("fontTools.feaLib.builder")
259*e1fe3e4aSElliott Hughes        with CapturingLogHandler(logger, "INFO") as captor:
260*e1fe3e4aSElliott Hughes            self.build(
261*e1fe3e4aSElliott Hughes                "feature test {"
262*e1fe3e4aSElliott Hughes                "    sub A by A.sc;"
263*e1fe3e4aSElliott Hughes                "    sub B by B.sc;"
264*e1fe3e4aSElliott Hughes                "    sub A by A.sc;"
265*e1fe3e4aSElliott Hughes                "} test;"
266*e1fe3e4aSElliott Hughes            )
267*e1fe3e4aSElliott Hughes        captor.assertRegex(
268*e1fe3e4aSElliott Hughes            'Removing duplicate single substitution from glyph "A" to "A.sc"'
269*e1fe3e4aSElliott Hughes        )
270*e1fe3e4aSElliott Hughes
271*e1fe3e4aSElliott Hughes    def test_multipleSubst_multipleSubstitutionsForSameGlyph(self):
272*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
273*e1fe3e4aSElliott Hughes            FeatureLibError,
274*e1fe3e4aSElliott Hughes            'Already defined substitution for glyph "f_f_i"',
275*e1fe3e4aSElliott Hughes            self.build,
276*e1fe3e4aSElliott Hughes            "feature test {"
277*e1fe3e4aSElliott Hughes            "    sub f_f_i by f f i;"
278*e1fe3e4aSElliott Hughes            "    sub c_t by c t;"
279*e1fe3e4aSElliott Hughes            "    sub f_f_i by f_f i;"
280*e1fe3e4aSElliott Hughes            "} test;",
281*e1fe3e4aSElliott Hughes        )
282*e1fe3e4aSElliott Hughes
283*e1fe3e4aSElliott Hughes    def test_multipleSubst_multipleIdenticalSubstitutionsForSameGlyph_info(self):
284*e1fe3e4aSElliott Hughes        logger = logging.getLogger("fontTools.feaLib.builder")
285*e1fe3e4aSElliott Hughes        with CapturingLogHandler(logger, "INFO") as captor:
286*e1fe3e4aSElliott Hughes            self.build(
287*e1fe3e4aSElliott Hughes                "feature test {"
288*e1fe3e4aSElliott Hughes                "    sub f_f_i by f f i;"
289*e1fe3e4aSElliott Hughes                "    sub c_t by c t;"
290*e1fe3e4aSElliott Hughes                "    sub f_f_i by f f i;"
291*e1fe3e4aSElliott Hughes                "} test;"
292*e1fe3e4aSElliott Hughes            )
293*e1fe3e4aSElliott Hughes        captor.assertRegex(
294*e1fe3e4aSElliott Hughes            r"Removing duplicate multiple substitution from glyph \"f_f_i\" to \('f', 'f', 'i'\)"
295*e1fe3e4aSElliott Hughes        )
296*e1fe3e4aSElliott Hughes
297*e1fe3e4aSElliott Hughes    def test_pairPos_redefinition_warning(self):
298*e1fe3e4aSElliott Hughes        # https://github.com/fonttools/fonttools/issues/1147
299*e1fe3e4aSElliott Hughes        logger = logging.getLogger("fontTools.otlLib.builder")
300*e1fe3e4aSElliott Hughes        with CapturingLogHandler(logger, "DEBUG") as captor:
301*e1fe3e4aSElliott Hughes            # the pair "yacute semicolon" is redefined in the enum pos
302*e1fe3e4aSElliott Hughes            font = self.build(
303*e1fe3e4aSElliott Hughes                "@Y_LC = [y yacute ydieresis];"
304*e1fe3e4aSElliott Hughes                "@SMALL_PUNC = [comma semicolon period];"
305*e1fe3e4aSElliott Hughes                "feature kern {"
306*e1fe3e4aSElliott Hughes                "  pos yacute semicolon -70;"
307*e1fe3e4aSElliott Hughes                "  enum pos @Y_LC semicolon -80;"
308*e1fe3e4aSElliott Hughes                "  pos @Y_LC @SMALL_PUNC -100;"
309*e1fe3e4aSElliott Hughes                "} kern;"
310*e1fe3e4aSElliott Hughes            )
311*e1fe3e4aSElliott Hughes
312*e1fe3e4aSElliott Hughes        captor.assertRegex("Already defined position for pair yacute semicolon")
313*e1fe3e4aSElliott Hughes
314*e1fe3e4aSElliott Hughes        # the first definition prevails: yacute semicolon -70
315*e1fe3e4aSElliott Hughes        st = font["GPOS"].table.LookupList.Lookup[0].SubTable[0]
316*e1fe3e4aSElliott Hughes        self.assertEqual(st.Coverage.glyphs[2], "yacute")
317*e1fe3e4aSElliott Hughes        self.assertEqual(st.PairSet[2].PairValueRecord[0].SecondGlyph, "semicolon")
318*e1fe3e4aSElliott Hughes        self.assertEqual(
319*e1fe3e4aSElliott Hughes            vars(st.PairSet[2].PairValueRecord[0].Value1), {"XAdvance": -70}
320*e1fe3e4aSElliott Hughes        )
321*e1fe3e4aSElliott Hughes
322*e1fe3e4aSElliott Hughes    def test_singleSubst_multipleSubstitutionsForSameGlyph(self):
323*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
324*e1fe3e4aSElliott Hughes            FeatureLibError,
325*e1fe3e4aSElliott Hughes            'Already defined rule for replacing glyph "e" by "E.sc"',
326*e1fe3e4aSElliott Hughes            self.build,
327*e1fe3e4aSElliott Hughes            "feature test {"
328*e1fe3e4aSElliott Hughes            "    sub [a-z] by [A.sc-Z.sc];"
329*e1fe3e4aSElliott Hughes            "    sub e by e.fina;"
330*e1fe3e4aSElliott Hughes            "} test;",
331*e1fe3e4aSElliott Hughes        )
332*e1fe3e4aSElliott Hughes
333*e1fe3e4aSElliott Hughes    def test_singlePos_redefinition(self):
334*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
335*e1fe3e4aSElliott Hughes            FeatureLibError,
336*e1fe3e4aSElliott Hughes            'Already defined different position for glyph "A"',
337*e1fe3e4aSElliott Hughes            self.build,
338*e1fe3e4aSElliott Hughes            "feature test { pos A 123; pos A 456; } test;",
339*e1fe3e4aSElliott Hughes        )
340*e1fe3e4aSElliott Hughes
341*e1fe3e4aSElliott Hughes    def test_feature_outside_aalt(self):
342*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
343*e1fe3e4aSElliott Hughes            FeatureLibError,
344*e1fe3e4aSElliott Hughes            'Feature references are only allowed inside "feature aalt"',
345*e1fe3e4aSElliott Hughes            self.build,
346*e1fe3e4aSElliott Hughes            "feature test { feature test; } test;",
347*e1fe3e4aSElliott Hughes        )
348*e1fe3e4aSElliott Hughes
349*e1fe3e4aSElliott Hughes    def test_feature_undefinedReference(self):
350*e1fe3e4aSElliott Hughes        with warnings.catch_warnings(record=True) as w:
351*e1fe3e4aSElliott Hughes            self.build("feature aalt { feature none; } aalt;")
352*e1fe3e4aSElliott Hughes            assert len(w) == 1
353*e1fe3e4aSElliott Hughes            assert "Feature none has not been defined" in str(w[0].message)
354*e1fe3e4aSElliott Hughes
355*e1fe3e4aSElliott Hughes    def test_GlyphClassDef_conflictingClasses(self):
356*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
357*e1fe3e4aSElliott Hughes            FeatureLibError,
358*e1fe3e4aSElliott Hughes            "Glyph X was assigned to a different class",
359*e1fe3e4aSElliott Hughes            self.build,
360*e1fe3e4aSElliott Hughes            "table GDEF {"
361*e1fe3e4aSElliott Hughes            "    GlyphClassDef [a b], [X], , ;"
362*e1fe3e4aSElliott Hughes            "    GlyphClassDef [a b X], , , ;"
363*e1fe3e4aSElliott Hughes            "} GDEF;",
364*e1fe3e4aSElliott Hughes        )
365*e1fe3e4aSElliott Hughes
366*e1fe3e4aSElliott Hughes    def test_languagesystem(self):
367*e1fe3e4aSElliott Hughes        builder = Builder(makeTTFont(), (None, None))
368*e1fe3e4aSElliott Hughes        builder.add_language_system(None, "latn", "FRA")
369*e1fe3e4aSElliott Hughes        builder.add_language_system(None, "cyrl", "RUS")
370*e1fe3e4aSElliott Hughes        builder.start_feature(location=None, name="test")
371*e1fe3e4aSElliott Hughes        self.assertEqual(builder.language_systems, {("latn", "FRA"), ("cyrl", "RUS")})
372*e1fe3e4aSElliott Hughes
373*e1fe3e4aSElliott Hughes    def test_languagesystem_duplicate(self):
374*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
375*e1fe3e4aSElliott Hughes            FeatureLibError,
376*e1fe3e4aSElliott Hughes            '"languagesystem cyrl RUS" has already been specified',
377*e1fe3e4aSElliott Hughes            self.build,
378*e1fe3e4aSElliott Hughes            "languagesystem cyrl RUS; languagesystem cyrl RUS;",
379*e1fe3e4aSElliott Hughes        )
380*e1fe3e4aSElliott Hughes
381*e1fe3e4aSElliott Hughes    def test_languagesystem_none_specified(self):
382*e1fe3e4aSElliott Hughes        builder = Builder(makeTTFont(), (None, None))
383*e1fe3e4aSElliott Hughes        builder.start_feature(location=None, name="test")
384*e1fe3e4aSElliott Hughes        self.assertEqual(builder.language_systems, {("DFLT", "dflt")})
385*e1fe3e4aSElliott Hughes
386*e1fe3e4aSElliott Hughes    def test_languagesystem_DFLT_dflt_not_first(self):
387*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
388*e1fe3e4aSElliott Hughes            FeatureLibError,
389*e1fe3e4aSElliott Hughes            'If "languagesystem DFLT dflt" is present, '
390*e1fe3e4aSElliott Hughes            "it must be the first of the languagesystem statements",
391*e1fe3e4aSElliott Hughes            self.build,
392*e1fe3e4aSElliott Hughes            "languagesystem latn TRK; languagesystem DFLT dflt;",
393*e1fe3e4aSElliott Hughes        )
394*e1fe3e4aSElliott Hughes
395*e1fe3e4aSElliott Hughes    def test_languagesystem_DFLT_not_preceding(self):
396*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
397*e1fe3e4aSElliott Hughes            FeatureLibError,
398*e1fe3e4aSElliott Hughes            'languagesystems using the "DFLT" script tag must '
399*e1fe3e4aSElliott Hughes            "precede all other languagesystems",
400*e1fe3e4aSElliott Hughes            self.build,
401*e1fe3e4aSElliott Hughes            "languagesystem DFLT dflt; "
402*e1fe3e4aSElliott Hughes            "languagesystem latn dflt; "
403*e1fe3e4aSElliott Hughes            "languagesystem DFLT fooo; ",
404*e1fe3e4aSElliott Hughes        )
405*e1fe3e4aSElliott Hughes
406*e1fe3e4aSElliott Hughes    def test_script(self):
407*e1fe3e4aSElliott Hughes        builder = Builder(makeTTFont(), (None, None))
408*e1fe3e4aSElliott Hughes        builder.start_feature(location=None, name="test")
409*e1fe3e4aSElliott Hughes        builder.set_script(location=None, script="cyrl")
410*e1fe3e4aSElliott Hughes        self.assertEqual(builder.language_systems, {("cyrl", "dflt")})
411*e1fe3e4aSElliott Hughes
412*e1fe3e4aSElliott Hughes    def test_script_in_aalt_feature(self):
413*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
414*e1fe3e4aSElliott Hughes            FeatureLibError,
415*e1fe3e4aSElliott Hughes            'Script statements are not allowed within "feature aalt"',
416*e1fe3e4aSElliott Hughes            self.build,
417*e1fe3e4aSElliott Hughes            "feature aalt { script latn; } aalt;",
418*e1fe3e4aSElliott Hughes        )
419*e1fe3e4aSElliott Hughes
420*e1fe3e4aSElliott Hughes    def test_script_in_size_feature(self):
421*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
422*e1fe3e4aSElliott Hughes            FeatureLibError,
423*e1fe3e4aSElliott Hughes            'Script statements are not allowed within "feature size"',
424*e1fe3e4aSElliott Hughes            self.build,
425*e1fe3e4aSElliott Hughes            "feature size { script latn; } size;",
426*e1fe3e4aSElliott Hughes        )
427*e1fe3e4aSElliott Hughes
428*e1fe3e4aSElliott Hughes    def test_script_in_standalone_lookup(self):
429*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
430*e1fe3e4aSElliott Hughes            FeatureLibError,
431*e1fe3e4aSElliott Hughes            "Script statements are not allowed within standalone lookup blocks",
432*e1fe3e4aSElliott Hughes            self.build,
433*e1fe3e4aSElliott Hughes            "lookup test { script latn; } test;",
434*e1fe3e4aSElliott Hughes        )
435*e1fe3e4aSElliott Hughes
436*e1fe3e4aSElliott Hughes    def test_language(self):
437*e1fe3e4aSElliott Hughes        builder = Builder(makeTTFont(), (None, None))
438*e1fe3e4aSElliott Hughes        builder.add_language_system(None, "latn", "FRA ")
439*e1fe3e4aSElliott Hughes        builder.start_feature(location=None, name="test")
440*e1fe3e4aSElliott Hughes        builder.set_script(location=None, script="cyrl")
441*e1fe3e4aSElliott Hughes        builder.set_language(
442*e1fe3e4aSElliott Hughes            location=None, language="RUS ", include_default=False, required=False
443*e1fe3e4aSElliott Hughes        )
444*e1fe3e4aSElliott Hughes        self.assertEqual(builder.language_systems, {("cyrl", "RUS ")})
445*e1fe3e4aSElliott Hughes        builder.set_language(
446*e1fe3e4aSElliott Hughes            location=None, language="BGR ", include_default=True, required=False
447*e1fe3e4aSElliott Hughes        )
448*e1fe3e4aSElliott Hughes        self.assertEqual(builder.language_systems, {("cyrl", "BGR ")})
449*e1fe3e4aSElliott Hughes        builder.start_feature(location=None, name="test2")
450*e1fe3e4aSElliott Hughes        self.assertEqual(builder.language_systems, {("latn", "FRA ")})
451*e1fe3e4aSElliott Hughes
452*e1fe3e4aSElliott Hughes    def test_language_in_aalt_feature(self):
453*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
454*e1fe3e4aSElliott Hughes            FeatureLibError,
455*e1fe3e4aSElliott Hughes            'Language statements are not allowed within "feature aalt"',
456*e1fe3e4aSElliott Hughes            self.build,
457*e1fe3e4aSElliott Hughes            "feature aalt { language FRA; } aalt;",
458*e1fe3e4aSElliott Hughes        )
459*e1fe3e4aSElliott Hughes
460*e1fe3e4aSElliott Hughes    def test_language_in_size_feature(self):
461*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
462*e1fe3e4aSElliott Hughes            FeatureLibError,
463*e1fe3e4aSElliott Hughes            'Language statements are not allowed within "feature size"',
464*e1fe3e4aSElliott Hughes            self.build,
465*e1fe3e4aSElliott Hughes            "feature size { language FRA; } size;",
466*e1fe3e4aSElliott Hughes        )
467*e1fe3e4aSElliott Hughes
468*e1fe3e4aSElliott Hughes    def test_language_in_standalone_lookup(self):
469*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
470*e1fe3e4aSElliott Hughes            FeatureLibError,
471*e1fe3e4aSElliott Hughes            "Language statements are not allowed within standalone lookup blocks",
472*e1fe3e4aSElliott Hughes            self.build,
473*e1fe3e4aSElliott Hughes            "lookup test { language FRA; } test;",
474*e1fe3e4aSElliott Hughes        )
475*e1fe3e4aSElliott Hughes
476*e1fe3e4aSElliott Hughes    def test_language_required_duplicate(self):
477*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
478*e1fe3e4aSElliott Hughes            FeatureLibError,
479*e1fe3e4aSElliott Hughes            r"Language FRA \(script latn\) has already specified "
480*e1fe3e4aSElliott Hughes            "feature scmp as its required feature",
481*e1fe3e4aSElliott Hughes            self.build,
482*e1fe3e4aSElliott Hughes            "feature scmp {"
483*e1fe3e4aSElliott Hughes            "    script latn;"
484*e1fe3e4aSElliott Hughes            "    language FRA required;"
485*e1fe3e4aSElliott Hughes            "    language DEU required;"
486*e1fe3e4aSElliott Hughes            "    substitute [a-z] by [A.sc-Z.sc];"
487*e1fe3e4aSElliott Hughes            "} scmp;"
488*e1fe3e4aSElliott Hughes            "feature test {"
489*e1fe3e4aSElliott Hughes            "    script latn;"
490*e1fe3e4aSElliott Hughes            "    language FRA required;"
491*e1fe3e4aSElliott Hughes            "    substitute [a-z] by [A.sc-Z.sc];"
492*e1fe3e4aSElliott Hughes            "} test;",
493*e1fe3e4aSElliott Hughes        )
494*e1fe3e4aSElliott Hughes
495*e1fe3e4aSElliott Hughes    def test_lookup_already_defined(self):
496*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
497*e1fe3e4aSElliott Hughes            FeatureLibError,
498*e1fe3e4aSElliott Hughes            'Lookup "foo" has already been defined',
499*e1fe3e4aSElliott Hughes            self.build,
500*e1fe3e4aSElliott Hughes            "lookup foo {} foo; lookup foo {} foo;",
501*e1fe3e4aSElliott Hughes        )
502*e1fe3e4aSElliott Hughes
503*e1fe3e4aSElliott Hughes    def test_lookup_multiple_flags(self):
504*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
505*e1fe3e4aSElliott Hughes            FeatureLibError,
506*e1fe3e4aSElliott Hughes            "Within a named lookup block, all rules must be "
507*e1fe3e4aSElliott Hughes            "of the same lookup type and flag",
508*e1fe3e4aSElliott Hughes            self.build,
509*e1fe3e4aSElliott Hughes            "lookup foo {"
510*e1fe3e4aSElliott Hughes            "    lookupflag 1;"
511*e1fe3e4aSElliott Hughes            "    sub f i by f_i;"
512*e1fe3e4aSElliott Hughes            "    lookupflag 2;"
513*e1fe3e4aSElliott Hughes            "    sub f f i by f_f_i;"
514*e1fe3e4aSElliott Hughes            "} foo;",
515*e1fe3e4aSElliott Hughes        )
516*e1fe3e4aSElliott Hughes
517*e1fe3e4aSElliott Hughes    def test_lookup_multiple_types(self):
518*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
519*e1fe3e4aSElliott Hughes            FeatureLibError,
520*e1fe3e4aSElliott Hughes            "Within a named lookup block, all rules must be "
521*e1fe3e4aSElliott Hughes            "of the same lookup type and flag",
522*e1fe3e4aSElliott Hughes            self.build,
523*e1fe3e4aSElliott Hughes            "lookup foo {"
524*e1fe3e4aSElliott Hughes            "    sub f f i by f_f_i;"
525*e1fe3e4aSElliott Hughes            "    sub A from [A.alt1 A.alt2];"
526*e1fe3e4aSElliott Hughes            "} foo;",
527*e1fe3e4aSElliott Hughes        )
528*e1fe3e4aSElliott Hughes
529*e1fe3e4aSElliott Hughes    def test_lookup_inside_feature_aalt(self):
530*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
531*e1fe3e4aSElliott Hughes            FeatureLibError,
532*e1fe3e4aSElliott Hughes            "Lookup blocks cannot be placed inside 'aalt' features",
533*e1fe3e4aSElliott Hughes            self.build,
534*e1fe3e4aSElliott Hughes            "feature aalt {lookup L {} L;} aalt;",
535*e1fe3e4aSElliott Hughes        )
536*e1fe3e4aSElliott Hughes
537*e1fe3e4aSElliott Hughes    def test_chain_subst_refrences_GPOS_looup(self):
538*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
539*e1fe3e4aSElliott Hughes            FeatureLibError,
540*e1fe3e4aSElliott Hughes            "Missing index of the specified lookup, might be a positioning lookup",
541*e1fe3e4aSElliott Hughes            self.build,
542*e1fe3e4aSElliott Hughes            "lookup dummy { pos a 50; } dummy;"
543*e1fe3e4aSElliott Hughes            "feature test {"
544*e1fe3e4aSElliott Hughes            "    sub a' lookup dummy b;"
545*e1fe3e4aSElliott Hughes            "} test;",
546*e1fe3e4aSElliott Hughes        )
547*e1fe3e4aSElliott Hughes
548*e1fe3e4aSElliott Hughes    def test_chain_pos_refrences_GSUB_looup(self):
549*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
550*e1fe3e4aSElliott Hughes            FeatureLibError,
551*e1fe3e4aSElliott Hughes            "Missing index of the specified lookup, might be a substitution lookup",
552*e1fe3e4aSElliott Hughes            self.build,
553*e1fe3e4aSElliott Hughes            "lookup dummy { sub a by A; } dummy;"
554*e1fe3e4aSElliott Hughes            "feature test {"
555*e1fe3e4aSElliott Hughes            "    pos a' lookup dummy b;"
556*e1fe3e4aSElliott Hughes            "} test;",
557*e1fe3e4aSElliott Hughes        )
558*e1fe3e4aSElliott Hughes
559*e1fe3e4aSElliott Hughes    def test_STAT_elidedfallbackname_already_defined(self):
560*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
561*e1fe3e4aSElliott Hughes            FeatureLibError,
562*e1fe3e4aSElliott Hughes            "ElidedFallbackName is already set.",
563*e1fe3e4aSElliott Hughes            self.build,
564*e1fe3e4aSElliott Hughes            "table name {"
565*e1fe3e4aSElliott Hughes            '   nameid 256 "Roman"; '
566*e1fe3e4aSElliott Hughes            "} name;"
567*e1fe3e4aSElliott Hughes            "table STAT {"
568*e1fe3e4aSElliott Hughes            '    ElidedFallbackName { name "Roman"; };'
569*e1fe3e4aSElliott Hughes            "    ElidedFallbackNameID 256;"
570*e1fe3e4aSElliott Hughes            "} STAT;",
571*e1fe3e4aSElliott Hughes        )
572*e1fe3e4aSElliott Hughes
573*e1fe3e4aSElliott Hughes    def test_STAT_elidedfallbackname_set_twice(self):
574*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
575*e1fe3e4aSElliott Hughes            FeatureLibError,
576*e1fe3e4aSElliott Hughes            "ElidedFallbackName is already set.",
577*e1fe3e4aSElliott Hughes            self.build,
578*e1fe3e4aSElliott Hughes            "table name {"
579*e1fe3e4aSElliott Hughes            '   nameid 256 "Roman"; '
580*e1fe3e4aSElliott Hughes            "} name;"
581*e1fe3e4aSElliott Hughes            "table STAT {"
582*e1fe3e4aSElliott Hughes            '    ElidedFallbackName { name "Roman"; };'
583*e1fe3e4aSElliott Hughes            '    ElidedFallbackName { name "Italic"; };'
584*e1fe3e4aSElliott Hughes            "} STAT;",
585*e1fe3e4aSElliott Hughes        )
586*e1fe3e4aSElliott Hughes
587*e1fe3e4aSElliott Hughes    def test_STAT_elidedfallbacknameID_already_defined(self):
588*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
589*e1fe3e4aSElliott Hughes            FeatureLibError,
590*e1fe3e4aSElliott Hughes            "ElidedFallbackNameID is already set.",
591*e1fe3e4aSElliott Hughes            self.build,
592*e1fe3e4aSElliott Hughes            "table name {"
593*e1fe3e4aSElliott Hughes            '   nameid 256 "Roman"; '
594*e1fe3e4aSElliott Hughes            "} name;"
595*e1fe3e4aSElliott Hughes            "table STAT {"
596*e1fe3e4aSElliott Hughes            "    ElidedFallbackNameID 256;"
597*e1fe3e4aSElliott Hughes            '    ElidedFallbackName { name "Roman"; };'
598*e1fe3e4aSElliott Hughes            "} STAT;",
599*e1fe3e4aSElliott Hughes        )
600*e1fe3e4aSElliott Hughes
601*e1fe3e4aSElliott Hughes    def test_STAT_elidedfallbacknameID_not_in_name_table(self):
602*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
603*e1fe3e4aSElliott Hughes            FeatureLibError,
604*e1fe3e4aSElliott Hughes            "ElidedFallbackNameID 256 points to a nameID that does not "
605*e1fe3e4aSElliott Hughes            'exist in the "name" table',
606*e1fe3e4aSElliott Hughes            self.build,
607*e1fe3e4aSElliott Hughes            "table name {"
608*e1fe3e4aSElliott Hughes            '   nameid 257 "Roman"; '
609*e1fe3e4aSElliott Hughes            "} name;"
610*e1fe3e4aSElliott Hughes            "table STAT {"
611*e1fe3e4aSElliott Hughes            "    ElidedFallbackNameID 256;"
612*e1fe3e4aSElliott Hughes            '    DesignAxis opsz 1 { name "Optical Size"; };'
613*e1fe3e4aSElliott Hughes            "} STAT;",
614*e1fe3e4aSElliott Hughes        )
615*e1fe3e4aSElliott Hughes
616*e1fe3e4aSElliott Hughes    def test_STAT_design_axis_name(self):
617*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
618*e1fe3e4aSElliott Hughes            FeatureLibError,
619*e1fe3e4aSElliott Hughes            'Expected "name"',
620*e1fe3e4aSElliott Hughes            self.build,
621*e1fe3e4aSElliott Hughes            "table name {"
622*e1fe3e4aSElliott Hughes            '   nameid 256 "Roman"; '
623*e1fe3e4aSElliott Hughes            "} name;"
624*e1fe3e4aSElliott Hughes            "table STAT {"
625*e1fe3e4aSElliott Hughes            '    ElidedFallbackName { name "Roman"; };'
626*e1fe3e4aSElliott Hughes            '    DesignAxis opsz 0 { badtag "Optical Size"; };'
627*e1fe3e4aSElliott Hughes            "} STAT;",
628*e1fe3e4aSElliott Hughes        )
629*e1fe3e4aSElliott Hughes
630*e1fe3e4aSElliott Hughes    def test_STAT_duplicate_design_axis_name(self):
631*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
632*e1fe3e4aSElliott Hughes            FeatureLibError,
633*e1fe3e4aSElliott Hughes            'DesignAxis already defined for tag "opsz".',
634*e1fe3e4aSElliott Hughes            self.build,
635*e1fe3e4aSElliott Hughes            "table name {"
636*e1fe3e4aSElliott Hughes            '   nameid 256 "Roman"; '
637*e1fe3e4aSElliott Hughes            "} name;"
638*e1fe3e4aSElliott Hughes            "table STAT {"
639*e1fe3e4aSElliott Hughes            '    ElidedFallbackName { name "Roman"; };'
640*e1fe3e4aSElliott Hughes            '    DesignAxis opsz 0 { name "Optical Size"; };'
641*e1fe3e4aSElliott Hughes            '    DesignAxis opsz 1 { name "Optical Size"; };'
642*e1fe3e4aSElliott Hughes            "} STAT;",
643*e1fe3e4aSElliott Hughes        )
644*e1fe3e4aSElliott Hughes
645*e1fe3e4aSElliott Hughes    def test_STAT_design_axis_duplicate_order(self):
646*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
647*e1fe3e4aSElliott Hughes            FeatureLibError,
648*e1fe3e4aSElliott Hughes            "DesignAxis already defined for axis number 0.",
649*e1fe3e4aSElliott Hughes            self.build,
650*e1fe3e4aSElliott Hughes            "table name {"
651*e1fe3e4aSElliott Hughes            '   nameid 256 "Roman"; '
652*e1fe3e4aSElliott Hughes            "} name;"
653*e1fe3e4aSElliott Hughes            "table STAT {"
654*e1fe3e4aSElliott Hughes            '    ElidedFallbackName { name "Roman"; };'
655*e1fe3e4aSElliott Hughes            '    DesignAxis opsz 0 { name "Optical Size"; };'
656*e1fe3e4aSElliott Hughes            '    DesignAxis wdth 0 { name "Width"; };'
657*e1fe3e4aSElliott Hughes            "    AxisValue {"
658*e1fe3e4aSElliott Hughes            "         location opsz 8;"
659*e1fe3e4aSElliott Hughes            "         location wdth 400;"
660*e1fe3e4aSElliott Hughes            '         name "Caption";'
661*e1fe3e4aSElliott Hughes            "     };"
662*e1fe3e4aSElliott Hughes            "} STAT;",
663*e1fe3e4aSElliott Hughes        )
664*e1fe3e4aSElliott Hughes
665*e1fe3e4aSElliott Hughes    def test_STAT_undefined_tag(self):
666*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
667*e1fe3e4aSElliott Hughes            FeatureLibError,
668*e1fe3e4aSElliott Hughes            "DesignAxis not defined for wdth.",
669*e1fe3e4aSElliott Hughes            self.build,
670*e1fe3e4aSElliott Hughes            "table name {"
671*e1fe3e4aSElliott Hughes            '   nameid 256 "Roman"; '
672*e1fe3e4aSElliott Hughes            "} name;"
673*e1fe3e4aSElliott Hughes            "table STAT {"
674*e1fe3e4aSElliott Hughes            '    ElidedFallbackName { name "Roman"; };'
675*e1fe3e4aSElliott Hughes            '    DesignAxis opsz 0 { name "Optical Size"; };'
676*e1fe3e4aSElliott Hughes            "    AxisValue { "
677*e1fe3e4aSElliott Hughes            "        location wdth 125; "
678*e1fe3e4aSElliott Hughes            '        name "Wide"; '
679*e1fe3e4aSElliott Hughes            "    };"
680*e1fe3e4aSElliott Hughes            "} STAT;",
681*e1fe3e4aSElliott Hughes        )
682*e1fe3e4aSElliott Hughes
683*e1fe3e4aSElliott Hughes    def test_STAT_axis_value_format4(self):
684*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
685*e1fe3e4aSElliott Hughes            FeatureLibError,
686*e1fe3e4aSElliott Hughes            "Axis tag wdth already defined.",
687*e1fe3e4aSElliott Hughes            self.build,
688*e1fe3e4aSElliott Hughes            "table name {"
689*e1fe3e4aSElliott Hughes            '   nameid 256 "Roman"; '
690*e1fe3e4aSElliott Hughes            "} name;"
691*e1fe3e4aSElliott Hughes            "table STAT {"
692*e1fe3e4aSElliott Hughes            '    ElidedFallbackName { name "Roman"; };'
693*e1fe3e4aSElliott Hughes            '    DesignAxis opsz 0 { name "Optical Size"; };'
694*e1fe3e4aSElliott Hughes            '    DesignAxis wdth 1 { name "Width"; };'
695*e1fe3e4aSElliott Hughes            '    DesignAxis wght 2 { name "Weight"; };'
696*e1fe3e4aSElliott Hughes            "    AxisValue { "
697*e1fe3e4aSElliott Hughes            "        location opsz 8; "
698*e1fe3e4aSElliott Hughes            "        location wdth 125; "
699*e1fe3e4aSElliott Hughes            "        location wdth 125; "
700*e1fe3e4aSElliott Hughes            "        location wght 500; "
701*e1fe3e4aSElliott Hughes            '        name "Caption Medium Wide"; '
702*e1fe3e4aSElliott Hughes            "    };"
703*e1fe3e4aSElliott Hughes            "} STAT;",
704*e1fe3e4aSElliott Hughes        )
705*e1fe3e4aSElliott Hughes
706*e1fe3e4aSElliott Hughes    def test_STAT_duplicate_axis_value_record(self):
707*e1fe3e4aSElliott Hughes        # Test for Duplicate AxisValueRecords even when the definition order
708*e1fe3e4aSElliott Hughes        # is different.
709*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
710*e1fe3e4aSElliott Hughes            FeatureLibError,
711*e1fe3e4aSElliott Hughes            "An AxisValueRecord with these values is already defined.",
712*e1fe3e4aSElliott Hughes            self.build,
713*e1fe3e4aSElliott Hughes            "table name {"
714*e1fe3e4aSElliott Hughes            '   nameid 256 "Roman"; '
715*e1fe3e4aSElliott Hughes            "} name;"
716*e1fe3e4aSElliott Hughes            "table STAT {"
717*e1fe3e4aSElliott Hughes            '    ElidedFallbackName { name "Roman"; };'
718*e1fe3e4aSElliott Hughes            '    DesignAxis opsz 0 { name "Optical Size"; };'
719*e1fe3e4aSElliott Hughes            '    DesignAxis wdth 1 { name "Width"; };'
720*e1fe3e4aSElliott Hughes            "    AxisValue {"
721*e1fe3e4aSElliott Hughes            "         location opsz 8;"
722*e1fe3e4aSElliott Hughes            "         location wdth 400;"
723*e1fe3e4aSElliott Hughes            '         name "Caption";'
724*e1fe3e4aSElliott Hughes            "     };"
725*e1fe3e4aSElliott Hughes            "    AxisValue {"
726*e1fe3e4aSElliott Hughes            "         location wdth 400;"
727*e1fe3e4aSElliott Hughes            "         location opsz 8;"
728*e1fe3e4aSElliott Hughes            '         name "Caption";'
729*e1fe3e4aSElliott Hughes            "     };"
730*e1fe3e4aSElliott Hughes            "} STAT;",
731*e1fe3e4aSElliott Hughes        )
732*e1fe3e4aSElliott Hughes
733*e1fe3e4aSElliott Hughes    def test_STAT_axis_value_missing_location(self):
734*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
735*e1fe3e4aSElliott Hughes            FeatureLibError,
736*e1fe3e4aSElliott Hughes            'Expected "Axis location"',
737*e1fe3e4aSElliott Hughes            self.build,
738*e1fe3e4aSElliott Hughes            "table name {"
739*e1fe3e4aSElliott Hughes            '   nameid 256 "Roman"; '
740*e1fe3e4aSElliott Hughes            "} name;"
741*e1fe3e4aSElliott Hughes            "table STAT {"
742*e1fe3e4aSElliott Hughes            '    ElidedFallbackName {   name "Roman"; '
743*e1fe3e4aSElliott Hughes            "};"
744*e1fe3e4aSElliott Hughes            '    DesignAxis opsz 0 { name "Optical Size"; };'
745*e1fe3e4aSElliott Hughes            "    AxisValue { "
746*e1fe3e4aSElliott Hughes            '        name "Wide"; '
747*e1fe3e4aSElliott Hughes            "    };"
748*e1fe3e4aSElliott Hughes            "} STAT;",
749*e1fe3e4aSElliott Hughes        )
750*e1fe3e4aSElliott Hughes
751*e1fe3e4aSElliott Hughes    def test_STAT_invalid_location_tag(self):
752*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
753*e1fe3e4aSElliott Hughes            FeatureLibError,
754*e1fe3e4aSElliott Hughes            "Tags cannot be longer than 4 characters",
755*e1fe3e4aSElliott Hughes            self.build,
756*e1fe3e4aSElliott Hughes            "table name {"
757*e1fe3e4aSElliott Hughes            '   nameid 256 "Roman"; '
758*e1fe3e4aSElliott Hughes            "} name;"
759*e1fe3e4aSElliott Hughes            "table STAT {"
760*e1fe3e4aSElliott Hughes            '    ElidedFallbackName { name "Roman"; '
761*e1fe3e4aSElliott Hughes            '                         name 3 1 0x0411 "ローマン"; }; '
762*e1fe3e4aSElliott Hughes            '    DesignAxis width 0 { name "Width"; };'
763*e1fe3e4aSElliott Hughes            "} STAT;",
764*e1fe3e4aSElliott Hughes        )
765*e1fe3e4aSElliott Hughes
766*e1fe3e4aSElliott Hughes    def test_extensions(self):
767*e1fe3e4aSElliott Hughes        class ast_BaseClass(ast.MarkClass):
768*e1fe3e4aSElliott Hughes            def asFea(self, indent=""):
769*e1fe3e4aSElliott Hughes                return ""
770*e1fe3e4aSElliott Hughes
771*e1fe3e4aSElliott Hughes        class ast_BaseClassDefinition(ast.MarkClassDefinition):
772*e1fe3e4aSElliott Hughes            def asFea(self, indent=""):
773*e1fe3e4aSElliott Hughes                return ""
774*e1fe3e4aSElliott Hughes
775*e1fe3e4aSElliott Hughes        class ast_MarkBasePosStatement(ast.MarkBasePosStatement):
776*e1fe3e4aSElliott Hughes            def asFea(self, indent=""):
777*e1fe3e4aSElliott Hughes                if isinstance(self.base, ast.MarkClassName):
778*e1fe3e4aSElliott Hughes                    res = ""
779*e1fe3e4aSElliott Hughes                    for bcd in self.base.markClass.definitions:
780*e1fe3e4aSElliott Hughes                        if res != "":
781*e1fe3e4aSElliott Hughes                            res += "\n{}".format(indent)
782*e1fe3e4aSElliott Hughes                        res += "pos base {} {}".format(
783*e1fe3e4aSElliott Hughes                            bcd.glyphs.asFea(), bcd.anchor.asFea()
784*e1fe3e4aSElliott Hughes                        )
785*e1fe3e4aSElliott Hughes                        for m in self.marks:
786*e1fe3e4aSElliott Hughes                            res += " mark @{}".format(m.name)
787*e1fe3e4aSElliott Hughes                        res += ";"
788*e1fe3e4aSElliott Hughes                else:
789*e1fe3e4aSElliott Hughes                    res = "pos base {}".format(self.base.asFea())
790*e1fe3e4aSElliott Hughes                    for a, m in self.marks:
791*e1fe3e4aSElliott Hughes                        res += " {} mark @{}".format(a.asFea(), m.name)
792*e1fe3e4aSElliott Hughes                    res += ";"
793*e1fe3e4aSElliott Hughes                return res
794*e1fe3e4aSElliott Hughes
795*e1fe3e4aSElliott Hughes        class testAst(object):
796*e1fe3e4aSElliott Hughes            MarkBasePosStatement = ast_MarkBasePosStatement
797*e1fe3e4aSElliott Hughes
798*e1fe3e4aSElliott Hughes            def __getattr__(self, name):
799*e1fe3e4aSElliott Hughes                return getattr(ast, name)
800*e1fe3e4aSElliott Hughes
801*e1fe3e4aSElliott Hughes        class testParser(Parser):
802*e1fe3e4aSElliott Hughes            def parse_position_base_(self, enumerated, vertical):
803*e1fe3e4aSElliott Hughes                location = self.cur_token_location_
804*e1fe3e4aSElliott Hughes                self.expect_keyword_("base")
805*e1fe3e4aSElliott Hughes                if enumerated:
806*e1fe3e4aSElliott Hughes                    raise FeatureLibError(
807*e1fe3e4aSElliott Hughes                        '"enumerate" is not allowed with '
808*e1fe3e4aSElliott Hughes                        "mark-to-base attachment positioning",
809*e1fe3e4aSElliott Hughes                        location,
810*e1fe3e4aSElliott Hughes                    )
811*e1fe3e4aSElliott Hughes                base = self.parse_glyphclass_(accept_glyphname=True)
812*e1fe3e4aSElliott Hughes                if self.next_token_ == "<":
813*e1fe3e4aSElliott Hughes                    marks = self.parse_anchor_marks_()
814*e1fe3e4aSElliott Hughes                else:
815*e1fe3e4aSElliott Hughes                    marks = []
816*e1fe3e4aSElliott Hughes                    while self.next_token_ == "mark":
817*e1fe3e4aSElliott Hughes                        self.expect_keyword_("mark")
818*e1fe3e4aSElliott Hughes                        m = self.expect_markClass_reference_()
819*e1fe3e4aSElliott Hughes                        marks.append(m)
820*e1fe3e4aSElliott Hughes                self.expect_symbol_(";")
821*e1fe3e4aSElliott Hughes                return self.ast.MarkBasePosStatement(base, marks, location=location)
822*e1fe3e4aSElliott Hughes
823*e1fe3e4aSElliott Hughes            def parseBaseClass(self):
824*e1fe3e4aSElliott Hughes                if not hasattr(self.doc_, "baseClasses"):
825*e1fe3e4aSElliott Hughes                    self.doc_.baseClasses = {}
826*e1fe3e4aSElliott Hughes                location = self.cur_token_location_
827*e1fe3e4aSElliott Hughes                glyphs = self.parse_glyphclass_(accept_glyphname=True)
828*e1fe3e4aSElliott Hughes                anchor = self.parse_anchor_()
829*e1fe3e4aSElliott Hughes                name = self.expect_class_name_()
830*e1fe3e4aSElliott Hughes                self.expect_symbol_(";")
831*e1fe3e4aSElliott Hughes                baseClass = self.doc_.baseClasses.get(name)
832*e1fe3e4aSElliott Hughes                if baseClass is None:
833*e1fe3e4aSElliott Hughes                    baseClass = ast_BaseClass(name)
834*e1fe3e4aSElliott Hughes                    self.doc_.baseClasses[name] = baseClass
835*e1fe3e4aSElliott Hughes                    self.glyphclasses_.define(name, baseClass)
836*e1fe3e4aSElliott Hughes                bcdef = ast_BaseClassDefinition(
837*e1fe3e4aSElliott Hughes                    baseClass, anchor, glyphs, location=location
838*e1fe3e4aSElliott Hughes                )
839*e1fe3e4aSElliott Hughes                baseClass.addDefinition(bcdef)
840*e1fe3e4aSElliott Hughes                return bcdef
841*e1fe3e4aSElliott Hughes
842*e1fe3e4aSElliott Hughes            extensions = {"baseClass": lambda s: s.parseBaseClass()}
843*e1fe3e4aSElliott Hughes            ast = testAst()
844*e1fe3e4aSElliott Hughes
845*e1fe3e4aSElliott Hughes        self.check_fea2fea_file(
846*e1fe3e4aSElliott Hughes            "baseClass.feax", base="baseClass.fea", parser=testParser
847*e1fe3e4aSElliott Hughes        )
848*e1fe3e4aSElliott Hughes
849*e1fe3e4aSElliott Hughes    def test_markClass_same_glyph_redefined(self):
850*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
851*e1fe3e4aSElliott Hughes            FeatureLibError,
852*e1fe3e4aSElliott Hughes            "Glyph acute already defined",
853*e1fe3e4aSElliott Hughes            self.build,
854*e1fe3e4aSElliott Hughes            "markClass [acute] <anchor 350 0> @TOP_MARKS;" * 2,
855*e1fe3e4aSElliott Hughes        )
856*e1fe3e4aSElliott Hughes
857*e1fe3e4aSElliott Hughes    def test_markClass_same_glyph_multiple_classes(self):
858*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
859*e1fe3e4aSElliott Hughes            FeatureLibError,
860*e1fe3e4aSElliott Hughes            "Glyph uni0327 cannot be in both @ogonek and @cedilla",
861*e1fe3e4aSElliott Hughes            self.build,
862*e1fe3e4aSElliott Hughes            "feature mark {"
863*e1fe3e4aSElliott Hughes            "    markClass [uni0327 uni0328] <anchor 0 0> @ogonek;"
864*e1fe3e4aSElliott Hughes            "    pos base [a] <anchor 399 0> mark @ogonek;"
865*e1fe3e4aSElliott Hughes            "    markClass [uni0327] <anchor 0 0> @cedilla;"
866*e1fe3e4aSElliott Hughes            "    pos base [a] <anchor 244 0> mark @cedilla;"
867*e1fe3e4aSElliott Hughes            "} mark;",
868*e1fe3e4aSElliott Hughes        )
869*e1fe3e4aSElliott Hughes
870*e1fe3e4aSElliott Hughes    def test_build_specific_tables(self):
871*e1fe3e4aSElliott Hughes        features = "feature liga {sub f i by f_i;} liga;"
872*e1fe3e4aSElliott Hughes        font = self.build(features)
873*e1fe3e4aSElliott Hughes        assert "GSUB" in font
874*e1fe3e4aSElliott Hughes
875*e1fe3e4aSElliott Hughes        font2 = self.build(features, tables=set())
876*e1fe3e4aSElliott Hughes        assert "GSUB" not in font2
877*e1fe3e4aSElliott Hughes
878*e1fe3e4aSElliott Hughes    def test_build_unsupported_tables(self):
879*e1fe3e4aSElliott Hughes        self.assertRaises(NotImplementedError, self.build, "", tables={"FOO"})
880*e1fe3e4aSElliott Hughes
881*e1fe3e4aSElliott Hughes    def test_build_pre_parsed_ast_featurefile(self):
882*e1fe3e4aSElliott Hughes        f = StringIO("feature liga {sub f i by f_i;} liga;")
883*e1fe3e4aSElliott Hughes        tree = Parser(f).parse()
884*e1fe3e4aSElliott Hughes        font = makeTTFont()
885*e1fe3e4aSElliott Hughes        addOpenTypeFeatures(font, tree)
886*e1fe3e4aSElliott Hughes        assert "GSUB" in font
887*e1fe3e4aSElliott Hughes
888*e1fe3e4aSElliott Hughes    def test_unsupported_subtable_break(self):
889*e1fe3e4aSElliott Hughes        logger = logging.getLogger("fontTools.otlLib.builder")
890*e1fe3e4aSElliott Hughes        with CapturingLogHandler(logger, level="WARNING") as captor:
891*e1fe3e4aSElliott Hughes            self.build(
892*e1fe3e4aSElliott Hughes                "feature test {"
893*e1fe3e4aSElliott Hughes                "    pos a 10;"
894*e1fe3e4aSElliott Hughes                "    subtable;"
895*e1fe3e4aSElliott Hughes                "    pos b 10;"
896*e1fe3e4aSElliott Hughes                "} test;"
897*e1fe3e4aSElliott Hughes            )
898*e1fe3e4aSElliott Hughes
899*e1fe3e4aSElliott Hughes        captor.assertRegex(
900*e1fe3e4aSElliott Hughes            '<features>:1:32: unsupported "subtable" statement for lookup type'
901*e1fe3e4aSElliott Hughes        )
902*e1fe3e4aSElliott Hughes
903*e1fe3e4aSElliott Hughes    def test_skip_featureNames_if_no_name_table(self):
904*e1fe3e4aSElliott Hughes        features = (
905*e1fe3e4aSElliott Hughes            "feature ss01 {"
906*e1fe3e4aSElliott Hughes            "    featureNames {"
907*e1fe3e4aSElliott Hughes            '        name "ignored as we request to skip name table";'
908*e1fe3e4aSElliott Hughes            "    };"
909*e1fe3e4aSElliott Hughes            "    sub A by A.alt1;"
910*e1fe3e4aSElliott Hughes            "} ss01;"
911*e1fe3e4aSElliott Hughes        )
912*e1fe3e4aSElliott Hughes        font = self.build(features, tables=["GSUB"])
913*e1fe3e4aSElliott Hughes        self.assertIn("GSUB", font)
914*e1fe3e4aSElliott Hughes        self.assertNotIn("name", font)
915*e1fe3e4aSElliott Hughes
916*e1fe3e4aSElliott Hughes    def test_singlePos_multiplePositionsForSameGlyph(self):
917*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
918*e1fe3e4aSElliott Hughes            FeatureLibError,
919*e1fe3e4aSElliott Hughes            "Already defined different position for glyph",
920*e1fe3e4aSElliott Hughes            self.build,
921*e1fe3e4aSElliott Hughes            "lookup foo {" "    pos A -45; " "    pos A 45; " "} foo;",
922*e1fe3e4aSElliott Hughes        )
923*e1fe3e4aSElliott Hughes
924*e1fe3e4aSElliott Hughes    def test_pairPos_enumRuleOverridenBySinglePair_DEBUG(self):
925*e1fe3e4aSElliott Hughes        logger = logging.getLogger("fontTools.otlLib.builder")
926*e1fe3e4aSElliott Hughes        with CapturingLogHandler(logger, "DEBUG") as captor:
927*e1fe3e4aSElliott Hughes            self.build(
928*e1fe3e4aSElliott Hughes                "feature test {"
929*e1fe3e4aSElliott Hughes                "    enum pos A [V Y] -80;"
930*e1fe3e4aSElliott Hughes                "    pos A V -75;"
931*e1fe3e4aSElliott Hughes                "} test;"
932*e1fe3e4aSElliott Hughes            )
933*e1fe3e4aSElliott Hughes        captor.assertRegex("Already defined position for pair A V at")
934*e1fe3e4aSElliott Hughes
935*e1fe3e4aSElliott Hughes    def test_ignore_empty_lookup_block(self):
936*e1fe3e4aSElliott Hughes        # https://github.com/fonttools/fonttools/pull/2277
937*e1fe3e4aSElliott Hughes        font = self.build(
938*e1fe3e4aSElliott Hughes            "lookup EMPTY { ; } EMPTY;" "feature ss01 { lookup EMPTY; } ss01;"
939*e1fe3e4aSElliott Hughes        )
940*e1fe3e4aSElliott Hughes        assert "GPOS" not in font
941*e1fe3e4aSElliott Hughes        assert "GSUB" not in font
942*e1fe3e4aSElliott Hughes
943*e1fe3e4aSElliott Hughes    def test_disable_empty_classes(self):
944*e1fe3e4aSElliott Hughes        for test in [
945*e1fe3e4aSElliott Hughes            "sub a by c []",
946*e1fe3e4aSElliott Hughes            "sub f f [] by f",
947*e1fe3e4aSElliott Hughes            "ignore sub a []'",
948*e1fe3e4aSElliott Hughes            "ignore sub [] a'",
949*e1fe3e4aSElliott Hughes            "sub a []' by b",
950*e1fe3e4aSElliott Hughes            "sub [] a' by b",
951*e1fe3e4aSElliott Hughes            "rsub [] by a",
952*e1fe3e4aSElliott Hughes            "pos [] 120",
953*e1fe3e4aSElliott Hughes            "pos a [] 120",
954*e1fe3e4aSElliott Hughes            "enum pos a [] 120",
955*e1fe3e4aSElliott Hughes            "pos cursive [] <anchor NULL> <anchor NULL>",
956*e1fe3e4aSElliott Hughes            "pos base [] <anchor NULL> mark @TOPMARKS",
957*e1fe3e4aSElliott Hughes            "pos ligature [] <anchor NULL> mark @TOPMARKS",
958*e1fe3e4aSElliott Hughes            "pos mark [] <anchor NULL> mark @TOPMARKS",
959*e1fe3e4aSElliott Hughes            "ignore pos a []'",
960*e1fe3e4aSElliott Hughes            "ignore pos [] a'",
961*e1fe3e4aSElliott Hughes        ]:
962*e1fe3e4aSElliott Hughes            self.assertRaisesRegex(
963*e1fe3e4aSElliott Hughes                FeatureLibError,
964*e1fe3e4aSElliott Hughes                "Empty ",
965*e1fe3e4aSElliott Hughes                self.build,
966*e1fe3e4aSElliott Hughes                f"markClass a <anchor 150 -10> @TOPMARKS; lookup foo {{ {test}; }} foo;",
967*e1fe3e4aSElliott Hughes            )
968*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
969*e1fe3e4aSElliott Hughes            FeatureLibError,
970*e1fe3e4aSElliott Hughes            "Empty glyph class in mark class definition",
971*e1fe3e4aSElliott Hughes            self.build,
972*e1fe3e4aSElliott Hughes            "markClass [] <anchor 150 -10> @TOPMARKS;",
973*e1fe3e4aSElliott Hughes        )
974*e1fe3e4aSElliott Hughes        self.assertRaisesRegex(
975*e1fe3e4aSElliott Hughes            FeatureLibError,
976*e1fe3e4aSElliott Hughes            'Expected a glyph class with 1 elements after "by", but found a glyph class with 0 elements',
977*e1fe3e4aSElliott Hughes            self.build,
978*e1fe3e4aSElliott Hughes            "feature test { sub a by []; test};",
979*e1fe3e4aSElliott Hughes        )
980*e1fe3e4aSElliott Hughes
981*e1fe3e4aSElliott Hughes    def test_unmarked_ignore_statement(self):
982*e1fe3e4aSElliott Hughes        name = "bug2949"
983*e1fe3e4aSElliott Hughes        logger = logging.getLogger("fontTools.feaLib.parser")
984*e1fe3e4aSElliott Hughes        with CapturingLogHandler(logger, level="WARNING") as captor:
985*e1fe3e4aSElliott Hughes            self.check_feature_file(name)
986*e1fe3e4aSElliott Hughes        self.check_fea2fea_file(name)
987*e1fe3e4aSElliott Hughes
988*e1fe3e4aSElliott Hughes        for line, sub in {(3, "sub"), (8, "pos"), (13, "sub")}:
989*e1fe3e4aSElliott Hughes            captor.assertRegex(
990*e1fe3e4aSElliott Hughes                f'{name}.fea:{line}:12: Ambiguous "ignore {sub}", there should be least one marked glyph'
991*e1fe3e4aSElliott Hughes            )
992*e1fe3e4aSElliott Hughes
993*e1fe3e4aSElliott Hughes    def test_conditionset_multiple_features(self):
994*e1fe3e4aSElliott Hughes        """Test that using the same `conditionset` for multiple features reuses the
995*e1fe3e4aSElliott Hughes        `FeatureVariationRecord`."""
996*e1fe3e4aSElliott Hughes
997*e1fe3e4aSElliott Hughes        features = """
998*e1fe3e4aSElliott Hughes            languagesystem DFLT dflt;
999*e1fe3e4aSElliott Hughes
1000*e1fe3e4aSElliott Hughes            conditionset test {
1001*e1fe3e4aSElliott Hughes                wght 600 1000;
1002*e1fe3e4aSElliott Hughes                wdth 150 200;
1003*e1fe3e4aSElliott Hughes            } test;
1004*e1fe3e4aSElliott Hughes
1005*e1fe3e4aSElliott Hughes            variation ccmp test {
1006*e1fe3e4aSElliott Hughes                sub e by a;
1007*e1fe3e4aSElliott Hughes            } ccmp;
1008*e1fe3e4aSElliott Hughes
1009*e1fe3e4aSElliott Hughes            variation rlig test {
1010*e1fe3e4aSElliott Hughes                sub b by c;
1011*e1fe3e4aSElliott Hughes            } rlig;
1012*e1fe3e4aSElliott Hughes        """
1013*e1fe3e4aSElliott Hughes
1014*e1fe3e4aSElliott Hughes        def make_mock_vf():
1015*e1fe3e4aSElliott Hughes            font = makeTTFont()
1016*e1fe3e4aSElliott Hughes            font["name"] = newTable("name")
1017*e1fe3e4aSElliott Hughes            addFvar(
1018*e1fe3e4aSElliott Hughes                font,
1019*e1fe3e4aSElliott Hughes                [("wght", 0, 0, 1000, "Weight"), ("wdth", 100, 100, 200, "Width")],
1020*e1fe3e4aSElliott Hughes                [],
1021*e1fe3e4aSElliott Hughes            )
1022*e1fe3e4aSElliott Hughes            del font["name"]
1023*e1fe3e4aSElliott Hughes            return font
1024*e1fe3e4aSElliott Hughes
1025*e1fe3e4aSElliott Hughes        font = make_mock_vf()
1026*e1fe3e4aSElliott Hughes        addOpenTypeFeaturesFromString(font, features)
1027*e1fe3e4aSElliott Hughes
1028*e1fe3e4aSElliott Hughes        table = font["GSUB"].table
1029*e1fe3e4aSElliott Hughes        assert table.FeatureVariations.FeatureVariationCount == 1
1030*e1fe3e4aSElliott Hughes
1031*e1fe3e4aSElliott Hughes        fvr = table.FeatureVariations.FeatureVariationRecord[0]
1032*e1fe3e4aSElliott Hughes        assert fvr.FeatureTableSubstitution.SubstitutionCount == 2
1033*e1fe3e4aSElliott Hughes
1034*e1fe3e4aSElliott Hughes    def test_condition_set_avar(self):
1035*e1fe3e4aSElliott Hughes        """Test that the `avar` table is consulted when normalizing user-space
1036*e1fe3e4aSElliott Hughes        values."""
1037*e1fe3e4aSElliott Hughes
1038*e1fe3e4aSElliott Hughes        features = """
1039*e1fe3e4aSElliott Hughes            languagesystem DFLT dflt;
1040*e1fe3e4aSElliott Hughes
1041*e1fe3e4aSElliott Hughes            lookup conditional_sub {
1042*e1fe3e4aSElliott Hughes                sub e by a;
1043*e1fe3e4aSElliott Hughes            } conditional_sub;
1044*e1fe3e4aSElliott Hughes
1045*e1fe3e4aSElliott Hughes            conditionset test {
1046*e1fe3e4aSElliott Hughes                wght 600 1000;
1047*e1fe3e4aSElliott Hughes                wdth 150 200;
1048*e1fe3e4aSElliott Hughes            } test;
1049*e1fe3e4aSElliott Hughes
1050*e1fe3e4aSElliott Hughes            variation rlig test {
1051*e1fe3e4aSElliott Hughes                lookup conditional_sub;
1052*e1fe3e4aSElliott Hughes            } rlig;
1053*e1fe3e4aSElliott Hughes        """
1054*e1fe3e4aSElliott Hughes
1055*e1fe3e4aSElliott Hughes        def make_mock_vf():
1056*e1fe3e4aSElliott Hughes            font = makeTTFont()
1057*e1fe3e4aSElliott Hughes            font["name"] = newTable("name")
1058*e1fe3e4aSElliott Hughes            addFvar(
1059*e1fe3e4aSElliott Hughes                font,
1060*e1fe3e4aSElliott Hughes                [("wght", 0, 0, 1000, "Weight"), ("wdth", 100, 100, 200, "Width")],
1061*e1fe3e4aSElliott Hughes                [],
1062*e1fe3e4aSElliott Hughes            )
1063*e1fe3e4aSElliott Hughes            del font["name"]
1064*e1fe3e4aSElliott Hughes            return font
1065*e1fe3e4aSElliott Hughes
1066*e1fe3e4aSElliott Hughes        # Without `avar`:
1067*e1fe3e4aSElliott Hughes        font = make_mock_vf()
1068*e1fe3e4aSElliott Hughes        addOpenTypeFeaturesFromString(font, features)
1069*e1fe3e4aSElliott Hughes        condition_table = (
1070*e1fe3e4aSElliott Hughes            font.tables["GSUB"]
1071*e1fe3e4aSElliott Hughes            .table.FeatureVariations.FeatureVariationRecord[0]
1072*e1fe3e4aSElliott Hughes            .ConditionSet.ConditionTable
1073*e1fe3e4aSElliott Hughes        )
1074*e1fe3e4aSElliott Hughes        # user-space wdth=150 and wght=600:
1075*e1fe3e4aSElliott Hughes        assert condition_table[0].FilterRangeMinValue == 0.5
1076*e1fe3e4aSElliott Hughes        assert condition_table[1].FilterRangeMinValue == 0.6
1077*e1fe3e4aSElliott Hughes
1078*e1fe3e4aSElliott Hughes        # With `avar`, shifting the wght axis' positive midpoint 0.5 a bit to
1079*e1fe3e4aSElliott Hughes        # the right, but leaving the wdth axis alone:
1080*e1fe3e4aSElliott Hughes        font = make_mock_vf()
1081*e1fe3e4aSElliott Hughes        font["avar"] = newTable("avar")
1082*e1fe3e4aSElliott Hughes        font["avar"].segments = {"wght": {-1.0: -1.0, 0.0: 0.0, 0.5: 0.625, 1.0: 1.0}}
1083*e1fe3e4aSElliott Hughes        addOpenTypeFeaturesFromString(font, features)
1084*e1fe3e4aSElliott Hughes        condition_table = (
1085*e1fe3e4aSElliott Hughes            font.tables["GSUB"]
1086*e1fe3e4aSElliott Hughes            .table.FeatureVariations.FeatureVariationRecord[0]
1087*e1fe3e4aSElliott Hughes            .ConditionSet.ConditionTable
1088*e1fe3e4aSElliott Hughes        )
1089*e1fe3e4aSElliott Hughes        # user-space wdth=150 as before and wght=600 shifted to the right:
1090*e1fe3e4aSElliott Hughes        assert condition_table[0].FilterRangeMinValue == 0.5
1091*e1fe3e4aSElliott Hughes        assert condition_table[1].FilterRangeMinValue == 0.7
1092*e1fe3e4aSElliott Hughes
1093*e1fe3e4aSElliott Hughes    def test_variable_scalar_avar(self):
1094*e1fe3e4aSElliott Hughes        """Test that the `avar` table is consulted when normalizing user-space
1095*e1fe3e4aSElliott Hughes        values."""
1096*e1fe3e4aSElliott Hughes
1097*e1fe3e4aSElliott Hughes        features = """
1098*e1fe3e4aSElliott Hughes            languagesystem DFLT dflt;
1099*e1fe3e4aSElliott Hughes
1100*e1fe3e4aSElliott Hughes            feature kern {
1101*e1fe3e4aSElliott Hughes                pos cursive one <anchor 0 (wght=200:12 wght=900:22 wdth=150,wght=900:42)> <anchor NULL>;
1102*e1fe3e4aSElliott Hughes                pos two <0 (wght=200:12 wght=900:22 wdth=150,wght=900:42) 0 0>;
1103*e1fe3e4aSElliott Hughes            } kern;
1104*e1fe3e4aSElliott Hughes        """
1105*e1fe3e4aSElliott Hughes
1106*e1fe3e4aSElliott Hughes        # Without `avar` (wght=200, wdth=100 is the default location):
1107*e1fe3e4aSElliott Hughes        font = self.make_mock_vf()
1108*e1fe3e4aSElliott Hughes        addOpenTypeFeaturesFromString(font, features)
1109*e1fe3e4aSElliott Hughes
1110*e1fe3e4aSElliott Hughes        var_region_list = font.tables["GDEF"].table.VarStore.VarRegionList
1111*e1fe3e4aSElliott Hughes        var_region_axis_wght = var_region_list.Region[0].VarRegionAxis[0]
1112*e1fe3e4aSElliott Hughes        var_region_axis_wdth = var_region_list.Region[0].VarRegionAxis[1]
1113*e1fe3e4aSElliott Hughes        assert self.get_region(var_region_axis_wght) == (0.0, 0.875, 0.875)
1114*e1fe3e4aSElliott Hughes        assert self.get_region(var_region_axis_wdth) == (0.0, 0.0, 0.0)
1115*e1fe3e4aSElliott Hughes        var_region_axis_wght = var_region_list.Region[1].VarRegionAxis[0]
1116*e1fe3e4aSElliott Hughes        var_region_axis_wdth = var_region_list.Region[1].VarRegionAxis[1]
1117*e1fe3e4aSElliott Hughes        assert self.get_region(var_region_axis_wght) == (0.0, 0.875, 0.875)
1118*e1fe3e4aSElliott Hughes        assert self.get_region(var_region_axis_wdth) == (0.0, 0.5, 0.5)
1119*e1fe3e4aSElliott Hughes
1120*e1fe3e4aSElliott Hughes        # With `avar`, shifting the wght axis' positive midpoint 0.5 a bit to
1121*e1fe3e4aSElliott Hughes        # the right, but leaving the wdth axis alone:
1122*e1fe3e4aSElliott Hughes        font = self.make_mock_vf()
1123*e1fe3e4aSElliott Hughes        font["avar"] = newTable("avar")
1124*e1fe3e4aSElliott Hughes        font["avar"].segments = {"wght": {-1.0: -1.0, 0.0: 0.0, 0.5: 0.625, 1.0: 1.0}}
1125*e1fe3e4aSElliott Hughes        addOpenTypeFeaturesFromString(font, features)
1126*e1fe3e4aSElliott Hughes
1127*e1fe3e4aSElliott Hughes        var_region_list = font.tables["GDEF"].table.VarStore.VarRegionList
1128*e1fe3e4aSElliott Hughes        var_region_axis_wght = var_region_list.Region[0].VarRegionAxis[0]
1129*e1fe3e4aSElliott Hughes        var_region_axis_wdth = var_region_list.Region[0].VarRegionAxis[1]
1130*e1fe3e4aSElliott Hughes        assert self.get_region(var_region_axis_wght) == (0.0, 0.90625, 0.90625)
1131*e1fe3e4aSElliott Hughes        assert self.get_region(var_region_axis_wdth) == (0.0, 0.0, 0.0)
1132*e1fe3e4aSElliott Hughes        var_region_axis_wght = var_region_list.Region[1].VarRegionAxis[0]
1133*e1fe3e4aSElliott Hughes        var_region_axis_wdth = var_region_list.Region[1].VarRegionAxis[1]
1134*e1fe3e4aSElliott Hughes        assert self.get_region(var_region_axis_wght) == (0.0, 0.90625, 0.90625)
1135*e1fe3e4aSElliott Hughes        assert self.get_region(var_region_axis_wdth) == (0.0, 0.5, 0.5)
1136*e1fe3e4aSElliott Hughes
1137*e1fe3e4aSElliott Hughes    def test_ligatureCaretByPos_variable_scalar(self):
1138*e1fe3e4aSElliott Hughes        """Test that the `avar` table is consulted when normalizing user-space
1139*e1fe3e4aSElliott Hughes        values."""
1140*e1fe3e4aSElliott Hughes
1141*e1fe3e4aSElliott Hughes        features = """
1142*e1fe3e4aSElliott Hughes            table GDEF {
1143*e1fe3e4aSElliott Hughes                LigatureCaretByPos f_i (wght=200:400 wght=900:1000) 380;
1144*e1fe3e4aSElliott Hughes            } GDEF;
1145*e1fe3e4aSElliott Hughes        """
1146*e1fe3e4aSElliott Hughes
1147*e1fe3e4aSElliott Hughes        font = self.make_mock_vf()
1148*e1fe3e4aSElliott Hughes        addOpenTypeFeaturesFromString(font, features)
1149*e1fe3e4aSElliott Hughes
1150*e1fe3e4aSElliott Hughes        table = font["GDEF"].table
1151*e1fe3e4aSElliott Hughes        lig_glyph = table.LigCaretList.LigGlyph[0]
1152*e1fe3e4aSElliott Hughes        assert lig_glyph.CaretValue[0].Format == 1
1153*e1fe3e4aSElliott Hughes        assert lig_glyph.CaretValue[0].Coordinate == 380
1154*e1fe3e4aSElliott Hughes        assert lig_glyph.CaretValue[1].Format == 3
1155*e1fe3e4aSElliott Hughes        assert lig_glyph.CaretValue[1].Coordinate == 400
1156*e1fe3e4aSElliott Hughes
1157*e1fe3e4aSElliott Hughes        var_region_list = table.VarStore.VarRegionList
1158*e1fe3e4aSElliott Hughes        var_region_axis = var_region_list.Region[0].VarRegionAxis[0]
1159*e1fe3e4aSElliott Hughes        assert self.get_region(var_region_axis) == (0.0, 0.875, 0.875)
1160*e1fe3e4aSElliott Hughes
1161*e1fe3e4aSElliott Hughes
1162*e1fe3e4aSElliott Hughesdef generate_feature_file_test(name):
1163*e1fe3e4aSElliott Hughes    return lambda self: self.check_feature_file(name)
1164*e1fe3e4aSElliott Hughes
1165*e1fe3e4aSElliott Hughes
1166*e1fe3e4aSElliott Hughesfor name in BuilderTest.TEST_FEATURE_FILES:
1167*e1fe3e4aSElliott Hughes    setattr(BuilderTest, "test_FeatureFile_%s" % name, generate_feature_file_test(name))
1168*e1fe3e4aSElliott Hughes
1169*e1fe3e4aSElliott Hughes
1170*e1fe3e4aSElliott Hughesdef generate_fea2fea_file_test(name):
1171*e1fe3e4aSElliott Hughes    return lambda self: self.check_fea2fea_file(name)
1172*e1fe3e4aSElliott Hughes
1173*e1fe3e4aSElliott Hughes
1174*e1fe3e4aSElliott Hughesfor name in BuilderTest.TEST_FEATURE_FILES:
1175*e1fe3e4aSElliott Hughes    setattr(
1176*e1fe3e4aSElliott Hughes        BuilderTest,
1177*e1fe3e4aSElliott Hughes        "test_Fea2feaFile_{}".format(name),
1178*e1fe3e4aSElliott Hughes        generate_fea2fea_file_test(name),
1179*e1fe3e4aSElliott Hughes    )
1180*e1fe3e4aSElliott Hughes
1181*e1fe3e4aSElliott Hughes
1182*e1fe3e4aSElliott Hughesif __name__ == "__main__":
1183*e1fe3e4aSElliott Hughes    sys.exit(unittest.main())
1184