1from __future__ import (
2    print_function,
3    division,
4    absolute_import,
5    unicode_literals,
6)
7from textwrap import dedent
8import logging
9import re
10from fontTools import ttLib
11from fontTools.feaLib.error import IncludedFeaNotFound, FeatureLibError
12from ufo2ft.featureWriters import (
13    BaseFeatureWriter,
14    KernFeatureWriter,
15    FEATURE_WRITERS_KEY,
16    ast,
17)
18from ufo2ft.featureCompiler import FeatureCompiler, parseLayoutFeatures, logger
19import py
20import pytest
21from .testSupport import pushd
22
23
24class ParseLayoutFeaturesTest(object):
25
26    def test_include(self, FontClass, tmpdir):
27        tmpdir.join("test.fea").write_text(
28            dedent(
29                """\
30            # hello world
31            """
32            ),
33            encoding="utf-8",
34        )
35        ufo = FontClass()
36        ufo.features.text = dedent(
37            """\
38            include(test.fea)
39            """
40        )
41        ufo.save(str(tmpdir.join("Test.ufo")))
42
43        fea = parseLayoutFeatures(ufo)
44
45        assert "# hello world" in str(fea)
46
47    def test_include_no_ufo_path(self, FontClass, tmpdir):
48        ufo = FontClass()
49        ufo.features.text = dedent(
50            """\
51            include(test.fea)
52            """
53        )
54        with pushd(str(tmpdir)):
55            with pytest.raises(IncludedFeaNotFound):
56                parseLayoutFeatures(ufo)
57
58    def test_include_not_found(self, FontClass, tmpdir, caplog):
59        caplog.set_level(logging.ERROR)
60
61        tmpdir.join("test.fea").write_text(
62            dedent(
63                """\
64            # hello world
65            """
66            ),
67            encoding="utf-8",
68        )
69        ufo = FontClass()
70        ufo.features.text = dedent(
71            """\
72            include(../test.fea)
73            """
74        )
75        ufo.save(str(tmpdir.join("Test.ufo")))
76
77        with caplog.at_level(logging.WARNING, logger=logger.name):
78            with pytest.raises(IncludedFeaNotFound):
79                parseLayoutFeatures(ufo)
80
81        assert len(caplog.records) == 1
82        assert "change the file name in the include" in caplog.text
83
84
85class FeatureCompilerTest(object):
86
87    def test_ttFont(self, FontClass):
88        ufo = FontClass()
89        ufo.newGlyph("f")
90        ufo.newGlyph("f_f")
91        ufo.features.text = dedent(
92            """\
93            feature liga {
94                sub f f by f_f;
95            } liga;
96            """
97        )
98        ttFont = ttLib.TTFont()
99        ttFont.setGlyphOrder(["f", "f_f"])
100
101        compiler = FeatureCompiler(ufo, ttFont)
102        compiler.compile()
103
104        assert "GSUB" in ttFont
105
106        gsub = ttFont["GSUB"].table
107        assert gsub.FeatureList.FeatureCount == 1
108        assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "liga"
109
110    def test_ttFont_None(self, FontClass):
111        ufo = FontClass()
112        ufo.newGlyph("f")
113        ufo.newGlyph("f_f")
114        ufo.features.text = dedent(
115            """\
116            feature liga {
117                sub f f by f_f;
118            } liga;
119            """
120        )
121
122        compiler = FeatureCompiler(ufo)
123        ttFont = compiler.compile()
124
125        assert "GSUB" in ttFont
126
127        gsub = ttFont["GSUB"].table
128        assert gsub.FeatureList.FeatureCount == 1
129        assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "liga"
130
131    def test_deprecated_methods(self, FontClass):
132        compiler = FeatureCompiler(FontClass())
133        with pytest.warns(UserWarning, match="method is deprecated"):
134            compiler.setupFile_features()
135
136        compiler.features = ""
137        with pytest.warns(UserWarning, match="method is deprecated"):
138            compiler.setupFile_featureTables()
139
140        class UserCompiler(FeatureCompiler):
141
142            def setupFile_features(self):
143                self.features = "# hello world"
144
145            def setupFile_featureTables(self):
146                self.ttFont = ttLib.TTFont()
147
148        compiler = UserCompiler(FontClass())
149        with pytest.warns(UserWarning, match="method is deprecated"):
150            compiler.compile()
151
152    def test_deprecated_mtiFeatures_argument(self, FontClass):
153        with pytest.warns(UserWarning, match="argument is ignored"):
154            FeatureCompiler(FontClass(), mtiFeatures="whatever")
155
156    def test_featureWriters_empty(self, FontClass):
157        kernWriter = KernFeatureWriter(ignoreMarks=False)
158        ufo = FontClass()
159        ufo.newGlyph("a")
160        ufo.newGlyph("v")
161        ufo.kerning.update({("a", "v"): -40})
162        compiler = FeatureCompiler(ufo, featureWriters=[kernWriter])
163        ttFont1 = compiler.compile()
164        assert "GPOS" in ttFont1
165
166        compiler = FeatureCompiler(ufo, featureWriters=[])
167        ttFont2 = compiler.compile()
168        assert "GPOS" not in ttFont2
169
170    def test_loadFeatureWriters_from_UFO_lib(self, FontClass):
171        ufo = FontClass()
172        ufo.newGlyph("a")
173        ufo.newGlyph("v")
174        ufo.kerning.update({("a", "v"): -40})
175        ufo.lib[FEATURE_WRITERS_KEY] = [{"class": "KernFeatureWriter"}]
176        compiler = FeatureCompiler(ufo)
177        ttFont = compiler.compile()
178
179        assert len(compiler.featureWriters) == 1
180        assert isinstance(compiler.featureWriters[0], KernFeatureWriter)
181        assert "GPOS" in ttFont
182
183    def test_GSUB_writers_run_first(self, FontClass):
184
185        class FooFeatureWriter(BaseFeatureWriter):
186
187            tableTag = "GSUB"
188
189            def write(self, font, feaFile, compiler=None):
190                foo = ast.FeatureBlock("FOO ")
191                foo.statements.append(
192                    ast.SingleSubstStatement(
193                        "a", "v", prefix="", suffix="", forceChain=None
194                    )
195                )
196                feaFile.statements.append(foo)
197
198        featureWriters = [KernFeatureWriter, FooFeatureWriter]
199
200        ufo = FontClass()
201        ufo.newGlyph("a")
202        ufo.newGlyph("v")
203        ufo.kerning.update({("a", "v"): -40})
204
205        compiler = FeatureCompiler(ufo, featureWriters=featureWriters)
206
207        assert len(compiler.featureWriters) == 2
208        assert compiler.featureWriters[0].tableTag == "GSUB"
209        assert compiler.featureWriters[1].tableTag == "GPOS"
210
211        ttFont = compiler.compile()
212
213        assert "GSUB" in ttFont
214
215        gsub = ttFont["GSUB"].table
216        assert gsub.FeatureList.FeatureCount == 1
217        assert gsub.FeatureList.FeatureRecord[0].FeatureTag == "FOO "
218
219    def test_buildTables_FeatureLibError(self, FontClass, caplog):
220        caplog.set_level(logging.CRITICAL)
221
222        ufo = FontClass()
223        ufo.newGlyph("f")
224        ufo.newGlyph("f.alt01")
225        features = dedent(
226            """\
227            feature BUGS {
228                # invalid
229                lookup MIXED_TYPE {
230                    sub f by f.alt01;
231                    sub f f by f_f;
232                } MIXED_TYPE;
233
234            } BUGS;
235            """
236        )
237        ufo.features.text = features
238
239        compiler = FeatureCompiler(ufo)
240
241        tmpfile = None
242        try:
243            with caplog.at_level(logging.ERROR, logger=logger.name):
244                with pytest.raises(FeatureLibError):
245                    compiler.compile()
246
247            assert len(caplog.records) == 1
248            assert "Compilation failed! Inspect temporary file" in caplog.text
249
250            tmpfile = py.path.local(re.findall(".*: '(.*)'$", caplog.text)[0])
251
252            assert tmpfile.exists()
253            assert tmpfile.read_text("utf-8") == features
254        finally:
255            if tmpfile is not None:
256                tmpfile.remove(ignore_errors=True)
257