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