1"""
2    test_build_html
3    ~~~~~~~~~~~~~~~
4
5    Test the HTML builder and check output against XPath.
6
7    :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
8    :license: BSD, see LICENSE for details.
9"""
10
11import os
12import subprocess
13from subprocess import PIPE, CalledProcessError
14from xml.etree import ElementTree
15
16import pytest
17
18from sphinx.util import docutils
19
20
21# check given command is runnable
22def runnable(command):
23    try:
24        subprocess.run(command, stdout=PIPE, stderr=PIPE, check=True)
25        return True
26    except (OSError, CalledProcessError):
27        return False  # command not found or exit with non-zero
28
29
30class EPUBElementTree:
31    """Test helper for content.opf and toc.ncx"""
32    namespaces = {
33        'idpf': 'http://www.idpf.org/2007/opf',
34        'dc': 'http://purl.org/dc/elements/1.1/',
35        'ibooks': 'http://vocabulary.itunes.apple.com/rdf/ibooks/vocabulary-extensions-1.0/',
36        'ncx': 'http://www.daisy.org/z3986/2005/ncx/',
37        'xhtml': 'http://www.w3.org/1999/xhtml',
38        'epub': 'http://www.idpf.org/2007/ops'
39    }
40
41    def __init__(self, tree):
42        self.tree = tree
43
44    @classmethod
45    def fromstring(cls, string):
46        return cls(ElementTree.fromstring(string))
47
48    def find(self, match):
49        ret = self.tree.find(match, namespaces=self.namespaces)
50        if ret is not None:
51            return self.__class__(ret)
52        else:
53            return ret
54
55    def findall(self, match):
56        ret = self.tree.findall(match, namespaces=self.namespaces)
57        return [self.__class__(e) for e in ret]
58
59    def __getattr__(self, name):
60        return getattr(self.tree, name)
61
62    def __iter__(self):
63        for child in self.tree:
64            yield self.__class__(child)
65
66
67@pytest.mark.sphinx('epub', testroot='basic')
68def test_build_epub(app):
69    app.build()
70    assert (app.outdir / 'mimetype').read_text() == 'application/epub+zip'
71    assert (app.outdir / 'META-INF' / 'container.xml').exists()
72
73    # toc.ncx
74    toc = EPUBElementTree.fromstring((app.outdir / 'toc.ncx').read_text())
75    assert toc.find("./ncx:docTitle/ncx:text").text == 'Python'
76
77    # toc.ncx / head
78    meta = list(toc.find("./ncx:head"))
79    assert meta[0].attrib == {'name': 'dtb:uid', 'content': 'unknown'}
80    assert meta[1].attrib == {'name': 'dtb:depth', 'content': '1'}
81    assert meta[2].attrib == {'name': 'dtb:totalPageCount', 'content': '0'}
82    assert meta[3].attrib == {'name': 'dtb:maxPageNumber', 'content': '0'}
83
84    # toc.ncx / navMap
85    navpoints = toc.findall("./ncx:navMap/ncx:navPoint")
86    assert len(navpoints) == 1
87    assert navpoints[0].attrib == {'id': 'navPoint1', 'playOrder': '1'}
88    assert navpoints[0].find("./ncx:content").attrib == {'src': 'index.xhtml'}
89
90    navlabel = navpoints[0].find("./ncx:navLabel/ncx:text")
91    assert navlabel.text == 'The basic Sphinx documentation for testing'
92
93    # content.opf
94    opf = EPUBElementTree.fromstring((app.outdir / 'content.opf').read_text())
95
96    # content.opf / metadata
97    metadata = opf.find("./idpf:metadata")
98    assert metadata.find("./dc:language").text == 'en'
99    assert metadata.find("./dc:title").text == 'Python'
100    assert metadata.find("./dc:description").text == 'unknown'
101    assert metadata.find("./dc:creator").text == 'unknown'
102    assert metadata.find("./dc:contributor").text == 'unknown'
103    assert metadata.find("./dc:publisher").text == 'unknown'
104    assert metadata.find("./dc:rights").text is None
105    assert metadata.find("./idpf:meta[@property='ibooks:version']").text is None
106    assert metadata.find("./idpf:meta[@property='ibooks:specified-fonts']").text == 'true'
107    assert metadata.find("./idpf:meta[@property='ibooks:binding']").text == 'true'
108    assert metadata.find("./idpf:meta[@property='ibooks:scroll-axis']").text == 'vertical'
109
110    # content.opf / manifest
111    manifest = opf.find("./idpf:manifest")
112    items = list(manifest)
113    assert items[0].attrib == {'id': 'ncx',
114                               'href': 'toc.ncx',
115                               'media-type': 'application/x-dtbncx+xml'}
116    assert items[1].attrib == {'id': 'nav',
117                               'href': 'nav.xhtml',
118                               'media-type': 'application/xhtml+xml',
119                               'properties': 'nav'}
120    assert items[2].attrib == {'id': 'epub-0',
121                               'href': 'genindex.xhtml',
122                               'media-type': 'application/xhtml+xml'}
123    assert items[3].attrib == {'id': 'epub-1',
124                               'href': 'index.xhtml',
125                               'media-type': 'application/xhtml+xml'}
126
127    for i, item in enumerate(items[2:]):
128        # items are named as epub-NN
129        assert item.get('id') == 'epub-%d' % i
130
131    # content.opf / spine
132    spine = opf.find("./idpf:spine")
133    itemrefs = list(spine)
134    assert spine.get('toc') == 'ncx'
135    assert spine.get('page-progression-direction') == 'ltr'
136    assert itemrefs[0].get('idref') == 'epub-1'
137    assert itemrefs[1].get('idref') == 'epub-0'
138
139    # content.opf / guide
140    reference = opf.find("./idpf:guide/idpf:reference")
141    assert reference.get('type') == 'toc'
142    assert reference.get('title') == 'Table of Contents'
143    assert reference.get('href') == 'index.xhtml'
144
145    # nav.xhtml
146    nav = EPUBElementTree.fromstring((app.outdir / 'nav.xhtml').read_text())
147    assert nav.attrib == {'lang': 'en',
148                          '{http://www.w3.org/XML/1998/namespace}lang': 'en'}
149    assert nav.find("./xhtml:head/xhtml:title").text == 'Table of Contents'
150
151    # nav.xhtml / nav
152    navlist = nav.find("./xhtml:body/xhtml:nav")
153    toc = navlist.findall("./xhtml:ol/xhtml:li")
154    assert navlist.find("./xhtml:h1").text == 'Table of Contents'
155    assert len(toc) == 1
156    assert toc[0].find("./xhtml:a").get("href") == 'index.xhtml'
157    assert toc[0].find("./xhtml:a").text == 'The basic Sphinx documentation for testing'
158
159
160@pytest.mark.sphinx('epub', testroot='footnotes',
161                    confoverrides={'epub_cover': ('_images/rimg.png', None)})
162def test_epub_cover(app):
163    app.build()
164
165    # content.opf / metadata
166    opf = EPUBElementTree.fromstring((app.outdir / 'content.opf').read_text())
167    cover_image = opf.find("./idpf:manifest/idpf:item[@href='%s']" % app.config.epub_cover[0])
168    cover = opf.find("./idpf:metadata/idpf:meta[@name='cover']")
169    assert cover
170    assert cover.get('content') == cover_image.get('id')
171
172
173@pytest.mark.sphinx('epub', testroot='toctree')
174def test_nested_toc(app):
175    app.build()
176
177    # toc.ncx
178    toc = EPUBElementTree.fromstring((app.outdir / 'toc.ncx').read_bytes())
179    assert toc.find("./ncx:docTitle/ncx:text").text == 'Python'
180
181    # toc.ncx / navPoint
182    def navinfo(elem):
183        label = elem.find("./ncx:navLabel/ncx:text")
184        content = elem.find("./ncx:content")
185        return (elem.get('id'), elem.get('playOrder'),
186                content.get('src'), label.text)
187
188    navpoints = toc.findall("./ncx:navMap/ncx:navPoint")
189    assert len(navpoints) == 4
190    assert navinfo(navpoints[0]) == ('navPoint1', '1', 'index.xhtml',
191                                     "Welcome to Sphinx Tests’s documentation!")
192    assert navpoints[0].findall("./ncx:navPoint") == []
193
194    # toc.ncx / nested navPoints
195    assert navinfo(navpoints[1]) == ('navPoint2', '2', 'foo.xhtml', 'foo')
196    navchildren = navpoints[1].findall("./ncx:navPoint")
197    assert len(navchildren) == 4
198    assert navinfo(navchildren[0]) == ('navPoint3', '2', 'foo.xhtml', 'foo')
199    assert navinfo(navchildren[1]) == ('navPoint4', '3', 'quux.xhtml', 'quux')
200    assert navinfo(navchildren[2]) == ('navPoint5', '4', 'foo.xhtml#foo-1', 'foo.1')
201    assert navinfo(navchildren[3]) == ('navPoint8', '6', 'foo.xhtml#foo-2', 'foo.2')
202
203    # nav.xhtml / nav
204    def navinfo(elem):
205        anchor = elem.find("./xhtml:a")
206        return (anchor.get('href'), anchor.text)
207
208    nav = EPUBElementTree.fromstring((app.outdir / 'nav.xhtml').read_bytes())
209    toc = nav.findall("./xhtml:body/xhtml:nav/xhtml:ol/xhtml:li")
210    assert len(toc) == 4
211    assert navinfo(toc[0]) == ('index.xhtml',
212                               "Welcome to Sphinx Tests’s documentation!")
213    assert toc[0].findall("./xhtml:ol") == []
214
215    # nav.xhtml / nested toc
216    assert navinfo(toc[1]) == ('foo.xhtml', 'foo')
217    tocchildren = toc[1].findall("./xhtml:ol/xhtml:li")
218    assert len(tocchildren) == 3
219    assert navinfo(tocchildren[0]) == ('quux.xhtml', 'quux')
220    assert navinfo(tocchildren[1]) == ('foo.xhtml#foo-1', 'foo.1')
221    assert navinfo(tocchildren[2]) == ('foo.xhtml#foo-2', 'foo.2')
222
223    grandchild = tocchildren[1].findall("./xhtml:ol/xhtml:li")
224    assert len(grandchild) == 1
225    assert navinfo(grandchild[0]) == ('foo.xhtml#foo-1-1', 'foo.1-1')
226
227
228@pytest.mark.sphinx('epub', testroot='need-escaped')
229def test_escaped_toc(app):
230    app.build()
231
232    # toc.ncx
233    toc = EPUBElementTree.fromstring((app.outdir / 'toc.ncx').read_bytes())
234    assert toc.find("./ncx:docTitle/ncx:text").text == 'need <b>"escaped"</b> project'
235
236    # toc.ncx / navPoint
237    def navinfo(elem):
238        label = elem.find("./ncx:navLabel/ncx:text")
239        content = elem.find("./ncx:content")
240        return (elem.get('id'), elem.get('playOrder'),
241                content.get('src'), label.text)
242
243    navpoints = toc.findall("./ncx:navMap/ncx:navPoint")
244    assert len(navpoints) == 4
245    assert navinfo(navpoints[0]) == ('navPoint1', '1', 'index.xhtml',
246                                     "Welcome to Sphinx Tests's documentation!")
247    assert navpoints[0].findall("./ncx:navPoint") == []
248
249    # toc.ncx / nested navPoints
250    assert navinfo(navpoints[1]) == ('navPoint2', '2', 'foo.xhtml', '<foo>')
251    navchildren = navpoints[1].findall("./ncx:navPoint")
252    assert len(navchildren) == 4
253    assert navinfo(navchildren[0]) == ('navPoint3', '2', 'foo.xhtml', '<foo>')
254    assert navinfo(navchildren[1]) == ('navPoint4', '3', 'quux.xhtml', 'quux')
255    assert navinfo(navchildren[2]) == ('navPoint5', '4', 'foo.xhtml#foo-1', 'foo “1”')
256    assert navinfo(navchildren[3]) == ('navPoint8', '6', 'foo.xhtml#foo-2', 'foo.2')
257
258    # nav.xhtml / nav
259    def navinfo(elem):
260        anchor = elem.find("./xhtml:a")
261        return (anchor.get('href'), anchor.text)
262
263    nav = EPUBElementTree.fromstring((app.outdir / 'nav.xhtml').read_bytes())
264    toc = nav.findall("./xhtml:body/xhtml:nav/xhtml:ol/xhtml:li")
265    assert len(toc) == 4
266    assert navinfo(toc[0]) == ('index.xhtml',
267                               "Welcome to Sphinx Tests's documentation!")
268    assert toc[0].findall("./xhtml:ol") == []
269
270    # nav.xhtml / nested toc
271    assert navinfo(toc[1]) == ('foo.xhtml', '<foo>')
272    tocchildren = toc[1].findall("./xhtml:ol/xhtml:li")
273    assert len(tocchildren) == 3
274    assert navinfo(tocchildren[0]) == ('quux.xhtml', 'quux')
275    assert navinfo(tocchildren[1]) == ('foo.xhtml#foo-1', 'foo “1”')
276    assert navinfo(tocchildren[2]) == ('foo.xhtml#foo-2', 'foo.2')
277
278    grandchild = tocchildren[1].findall("./xhtml:ol/xhtml:li")
279    assert len(grandchild) == 1
280    assert navinfo(grandchild[0]) == ('foo.xhtml#foo-1-1', 'foo.1-1')
281
282
283@pytest.mark.sphinx('epub', testroot='basic')
284def test_epub_writing_mode(app):
285    # horizontal (default)
286    app.build()
287
288    # horizontal / page-progression-direction
289    opf = EPUBElementTree.fromstring((app.outdir / 'content.opf').read_text())
290    assert opf.find("./idpf:spine").get('page-progression-direction') == 'ltr'
291
292    # horizontal / ibooks:scroll-axis
293    metadata = opf.find("./idpf:metadata")
294    assert metadata.find("./idpf:meta[@property='ibooks:scroll-axis']").text == 'vertical'
295
296    # horizontal / writing-mode (CSS)
297    css = (app.outdir / '_static' / 'epub.css').read_text()
298    assert 'writing-mode: horizontal-tb;' in css
299
300    # vertical
301    app.config.epub_writing_mode = 'vertical'
302    (app.outdir / 'index.xhtml').unlink()  # forcely rebuild
303    app.build()
304
305    # vertical / page-progression-direction
306    opf = EPUBElementTree.fromstring((app.outdir / 'content.opf').read_text())
307    assert opf.find("./idpf:spine").get('page-progression-direction') == 'rtl'
308
309    # vertical / ibooks:scroll-axis
310    metadata = opf.find("./idpf:metadata")
311    assert metadata.find("./idpf:meta[@property='ibooks:scroll-axis']").text == 'horizontal'
312
313    # vertical / writing-mode (CSS)
314    css = (app.outdir / '_static' / 'epub.css').read_text()
315    assert 'writing-mode: vertical-rl;' in css
316
317
318@pytest.mark.sphinx('epub', testroot='epub-anchor-id')
319def test_epub_anchor_id(app):
320    app.build()
321
322    html = (app.outdir / 'index.xhtml').read_text()
323    assert ('<p id="std-setting-STATICFILES_FINDERS">'
324            'blah blah blah</p>' in html)
325    assert ('<span id="std-setting-STATICFILES_SECTION"></span>'
326            '<h1>blah blah blah</h1>' in html)
327    assert 'see <a class="reference internal" href="#std-setting-STATICFILES_FINDERS">' in html
328
329
330@pytest.mark.sphinx('epub', testroot='html_assets')
331def test_epub_assets(app):
332    app.builder.build_all()
333
334    # epub_sytlesheets (same as html_css_files)
335    content = (app.outdir / 'index.xhtml').read_text()
336    assert ('<link rel="stylesheet" type="text/css" href="_static/css/style.css" />'
337            in content)
338    assert ('<link media="print" rel="stylesheet" title="title" type="text/css" '
339            'href="https://example.com/custom.css" />' in content)
340
341
342@pytest.mark.sphinx('epub', testroot='html_assets',
343                    confoverrides={'epub_css_files': ['css/epub.css']})
344def test_epub_css_files(app):
345    app.builder.build_all()
346
347    # epub_css_files
348    content = (app.outdir / 'index.xhtml').read_text()
349    assert '<link rel="stylesheet" type="text/css" href="_static/css/epub.css" />' in content
350
351    # files in html_css_files are not outputed
352    assert ('<link rel="stylesheet" type="text/css" href="_static/css/style.css" />'
353            not in content)
354    assert ('<link media="print" rel="stylesheet" title="title" type="text/css" '
355            'href="https://example.com/custom.css" />' not in content)
356
357
358@pytest.mark.skipif(docutils.__version_info__ < (0, 13),
359                    reason='docutils-0.13 or above is required')
360@pytest.mark.sphinx('epub', testroot='roles-download')
361def test_html_download_role(app, status, warning):
362    app.build()
363    assert not (app.outdir / '_downloads' / 'dummy.dat').exists()
364
365    content = (app.outdir / 'index.xhtml').read_text()
366    assert ('<li><p><code class="xref download docutils literal notranslate">'
367            '<span class="pre">dummy.dat</span></code></p></li>' in content)
368    assert ('<li><p><code class="xref download docutils literal notranslate">'
369            '<span class="pre">not_found.dat</span></code></p></li>' in content)
370    assert ('<li><p><code class="xref download docutils literal notranslate">'
371            '<span class="pre">Sphinx</span> <span class="pre">logo</span></code>'
372            '<span class="link-target"> [http://www.sphinx-doc.org/en/master'
373            '/_static/sphinxheader.png]</span></p></li>' in content)
374
375
376@pytest.mark.sphinx('epub', testroot='toctree-duplicated')
377def test_duplicated_toctree_entry(app, status, warning):
378    app.build()
379    assert 'WARNING: duplicated ToC entry found: foo.xhtml' in warning.getvalue()
380
381
382@pytest.mark.skipif('DO_EPUBCHECK' not in os.environ,
383                    reason='Skipped because DO_EPUBCHECK is not set')
384@pytest.mark.sphinx('epub')
385def test_run_epubcheck(app):
386    app.build()
387
388    epubcheck = os.environ.get('EPUBCHECK_PATH', '/usr/share/java/epubcheck.jar')
389    if runnable(['java', '-version']) and os.path.exists(epubcheck):
390        try:
391            subprocess.run(['java', '-jar', epubcheck, app.outdir / 'SphinxTests.epub'],
392                           stdout=PIPE, stderr=PIPE, check=True)
393        except CalledProcessError as exc:
394            print(exc.stdout.decode('utf-8'))
395            print(exc.stderr.decode('utf-8'))
396            assert False, 'epubcheck exited with return code %s' % exc.returncode
397