| # -*- coding: utf-8 -*- |
| """ |
| test_build_html |
| ~~~~~~~~~~~~~~~ |
| |
| Test the HTML builder and check output against XPath. |
| |
| :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. |
| :license: BSD, see LICENSE for details. |
| """ |
| |
| import os |
| import re |
| |
| from six import PY3, iteritems, StringIO |
| from six.moves import html_entities |
| |
| try: |
| import pygments |
| except ImportError: |
| pygments = None |
| |
| from sphinx import __version__ |
| from util import test_root, test_roots, remove_unicode_literals, gen_with_app, with_app |
| from etree13 import ElementTree as ET |
| |
| |
| def teardown_module(): |
| (test_root / '_build').rmtree(True) |
| |
| |
| html_warnfile = StringIO() |
| |
| ENV_WARNINGS = """\ |
| %(root)s/autodoc_fodder.py:docstring of autodoc_fodder\\.MarkupError:2: \ |
| WARNING: Explicit markup ends without a blank line; unexpected \ |
| unindent\\.\\n? |
| %(root)s/images.txt:9: WARNING: image file not readable: foo.png |
| %(root)s/images.txt:23: WARNING: nonlocal image URI found: \ |
| http://www.python.org/logo.png |
| %(root)s/includes.txt:\\d*: WARNING: Encoding 'utf-8-sig' used for \ |
| reading included file u'.*?wrongenc.inc' seems to be wrong, try giving an \ |
| :encoding: option\\n? |
| %(root)s/includes.txt:4: WARNING: download file not readable: .*?nonexisting.png |
| %(root)s/markup.txt:\\d+: WARNING: Malformed :option: u'Python c option', does \ |
| not contain option marker - or -- or / |
| """ |
| |
| HTML_WARNINGS = ENV_WARNINGS + """\ |
| %(root)s/images.txt:20: WARNING: no matching candidate for image URI u'foo.\\*' |
| None:\\d+: WARNING: citation not found: missing |
| %(root)s/markup.txt:: WARNING: invalid single index entry u'' |
| %(root)s/markup.txt:: WARNING: invalid pair index entry u'' |
| %(root)s/markup.txt:: WARNING: invalid pair index entry u'keyword; ' |
| """ |
| |
| if PY3: |
| ENV_WARNINGS = remove_unicode_literals(ENV_WARNINGS) |
| HTML_WARNINGS = remove_unicode_literals(HTML_WARNINGS) |
| |
| |
| def tail_check(check): |
| rex = re.compile(check) |
| def checker(nodes): |
| for node in nodes: |
| if node.tail and rex.search(node.tail): |
| return True |
| assert False, '%r not found in tail of any nodes %s' % (check, nodes) |
| return checker |
| |
| |
| HTML_XPATH = { |
| 'images.html': [ |
| (".//img[@src='_images/img.png']", ''), |
| (".//img[@src='_images/img1.png']", ''), |
| (".//img[@src='_images/simg.png']", ''), |
| (".//img[@src='_images/svgimg.svg']", ''), |
| ], |
| 'subdir/images.html': [ |
| (".//img[@src='../_images/img1.png']", ''), |
| (".//img[@src='../_images/rimg.png']", ''), |
| ], |
| 'subdir/includes.html': [ |
| (".//a[@href='../_downloads/img.png']", ''), |
| (".//img[@src='../_images/img.png']", ''), |
| (".//p", 'This is an include file.'), |
| ], |
| 'includes.html': [ |
| (".//pre", u'Max Strauß'), |
| (".//a[@href='_downloads/img.png']", ''), |
| (".//a[@href='_downloads/img1.png']", ''), |
| (".//pre", u'"quotes"'), |
| (".//pre", u"'included'"), |
| ], |
| 'autodoc.html': [ |
| (".//dt[@id='test_autodoc.Class']", ''), |
| (".//dt[@id='test_autodoc.function']/em", r'\*\*kwds'), |
| (".//dd/p", r'Return spam\.'), |
| ], |
| 'extapi.html': [ |
| (".//strong", 'from function: Foo'), |
| (".//strong", 'from class: Bar'), |
| ], |
| 'markup.html': [ |
| (".//title", 'set by title directive'), |
| (".//p/em", 'Section author: Georg Brandl'), |
| (".//p/em", 'Module author: Georg Brandl'), |
| # created by the meta directive |
| (".//meta[@name='author'][@content='Me']", ''), |
| (".//meta[@name='keywords'][@content='docs, sphinx']", ''), |
| # a label created by ``.. _label:`` |
| (".//div[@id='label']", ''), |
| # code with standard code blocks |
| (".//pre", '^some code$'), |
| # an option list |
| (".//span[@class='option']", '--help'), |
| # admonitions |
| (".//p[@class='first admonition-title']", 'My Admonition'), |
| (".//p[@class='last']", 'Note text.'), |
| (".//p[@class='last']", 'Warning text.'), |
| # inline markup |
| (".//li/strong", r'^command\\n$'), |
| (".//li/strong", r'^program\\n$'), |
| (".//li/em", r'^dfn\\n$'), |
| (".//li/code/span[@class='pre']", r'^kbd\\n$'), |
| (".//li/em", u'File \N{TRIANGULAR BULLET} Close'), |
| (".//li/code/span[@class='pre']", '^a/$'), |
| (".//li/code/em/span[@class='pre']", '^varpart$'), |
| (".//li/code/em/span[@class='pre']", '^i$'), |
| (".//a[@href='http://www.python.org/dev/peps/pep-0008']" |
| "[@class='pep reference external']/strong", 'PEP 8'), |
| (".//a[@href='http://www.python.org/dev/peps/pep-0008']" |
| "[@class='pep reference external']/strong", 'Python Enhancement Proposal #8'), |
| (".//a[@href='http://tools.ietf.org/html/rfc1.html']" |
| "[@class='rfc reference external']/strong", 'RFC 1'), |
| (".//a[@href='http://tools.ietf.org/html/rfc1.html']" |
| "[@class='rfc reference external']/strong", 'Request for Comments #1'), |
| (".//a[@href='objects.html#envvar-HOME']" |
| "[@class='reference internal']/code/span[@class='pre']", 'HOME'), |
| (".//a[@href='#with']" |
| "[@class='reference internal']/code/span[@class='pre']", '^with$'), |
| (".//a[@href='#grammar-token-try_stmt']" |
| "[@class='reference internal']/code/span", '^statement$'), |
| (".//a[@href='subdir/includes.html']" |
| "[@class='reference internal']/em", 'Including in subdir'), |
| (".//a[@href='objects.html#cmdoption-python-c']" |
| "[@class='reference internal']/em", 'Python -c option'), |
| # abbreviations |
| (".//abbr[@title='abbreviation']", '^abbr$'), |
| # version stuff |
| (".//div[@class='versionadded']/p/span", 'New in version 0.6: '), |
| (".//div[@class='versionadded']/p/span", |
| tail_check('First paragraph of versionadded')), |
| (".//div[@class='versionchanged']/p/span", |
| tail_check('First paragraph of versionchanged')), |
| (".//div[@class='versionchanged']/p", |
| 'Second paragraph of versionchanged'), |
| # footnote reference |
| (".//a[@class='footnote-reference']", r'\[1\]'), |
| # created by reference lookup |
| (".//a[@href='contents.html#ref1']", ''), |
| # ``seealso`` directive |
| (".//div/p[@class='first admonition-title']", 'See also'), |
| # a ``hlist`` directive |
| (".//table[@class='hlist']/tr/td/ul/li", '^This$'), |
| # a ``centered`` directive |
| (".//p[@class='centered']/strong", 'LICENSE'), |
| # a glossary |
| (".//dl/dt[@id='term-boson']", 'boson'), |
| # a production list |
| (".//pre/strong", 'try_stmt'), |
| (".//pre/a[@href='#grammar-token-try1_stmt']/code/span", 'try1_stmt'), |
| # tests for ``only`` directive |
| (".//p", 'A global substitution.'), |
| (".//p", 'In HTML.'), |
| (".//p", 'In both.'), |
| (".//p", 'Always present'), |
| ], |
| 'objects.html': [ |
| (".//dt[@id='mod.Cls.meth1']", ''), |
| (".//dt[@id='errmod.Error']", ''), |
| (".//dt/code", r'long\(parameter,\s* list\)'), |
| (".//dt/code", 'another one'), |
| (".//a[@href='#mod.Cls'][@class='reference internal']", ''), |
| (".//dl[@class='userdesc']", ''), |
| (".//dt[@id='userdesc-myobj']", ''), |
| (".//a[@href='#userdesc-myobj'][@class='reference internal']", ''), |
| # C references |
| (".//span[@class='pre']", 'CFunction()'), |
| (".//a[@href='#c.Sphinx_DoSomething']", ''), |
| (".//a[@href='#c.SphinxStruct.member']", ''), |
| (".//a[@href='#c.SPHINX_USE_PYTHON']", ''), |
| (".//a[@href='#c.SphinxType']", ''), |
| (".//a[@href='#c.sphinx_global']", ''), |
| # test global TOC created by toctree() |
| (".//ul[@class='current']/li[@class='toctree-l1 current']/a[@href='']", |
| 'Testing object descriptions'), |
| (".//li[@class='toctree-l1']/a[@href='markup.html']", |
| 'Testing various markup'), |
| # test unknown field names |
| (".//th[@class='field-name']", 'Field_name:'), |
| (".//th[@class='field-name']", 'Field_name all lower:'), |
| (".//th[@class='field-name']", 'FIELD_NAME:'), |
| (".//th[@class='field-name']", 'FIELD_NAME ALL CAPS:'), |
| (".//th[@class='field-name']", 'Field_Name:'), |
| (".//th[@class='field-name']", 'Field_Name All Word Caps:'), |
| (".//th[@class='field-name']", 'Field_name:'), |
| (".//th[@class='field-name']", 'Field_name First word cap:'), |
| (".//th[@class='field-name']", 'FIELd_name:'), |
| (".//th[@class='field-name']", 'FIELd_name PARTial caps:'), |
| # custom sidebar |
| (".//h4", 'Custom sidebar'), |
| # docfields |
| (".//td[@class='field-body']/strong", '^moo$'), |
| (".//td[@class='field-body']/strong", |
| tail_check(r'\(Moo\) .* Moo')), |
| (".//td[@class='field-body']/ul/li/strong", '^hour$'), |
| (".//td[@class='field-body']/ul/li/em", '^DuplicateType$'), |
| (".//td[@class='field-body']/ul/li/em", |
| tail_check(r'.* Some parameter')), |
| ], |
| 'contents.html': [ |
| (".//meta[@name='hc'][@content='hcval']", ''), |
| (".//meta[@name='hc_co'][@content='hcval_co']", ''), |
| (".//meta[@name='testopt'][@content='testoverride']", ''), |
| (".//td[@class='label']", r'\[Ref1\]'), |
| (".//td[@class='label']", ''), |
| (".//li[@class='toctree-l1']/a", 'Testing various markup'), |
| (".//li[@class='toctree-l2']/a", 'Inline markup'), |
| (".//title", 'Sphinx <Tests>'), |
| (".//div[@class='footer']", 'Georg Brandl & Team'), |
| (".//a[@href='http://python.org/']" |
| "[@class='reference external']", ''), |
| (".//li/a[@href='genindex.html']/em", 'Index'), |
| (".//li/a[@href='py-modindex.html']/em", 'Module Index'), |
| (".//li/a[@href='search.html']/em", 'Search Page'), |
| # custom sidebar only for contents |
| (".//h4", 'Contents sidebar'), |
| # custom JavaScript |
| (".//script[@src='file://moo.js']", ''), |
| ], |
| 'bom.html': [ |
| (".//title", " File with UTF-8 BOM"), |
| ], |
| 'extensions.html': [ |
| (".//a[@href='http://python.org/dev/']", "http://python.org/dev/"), |
| (".//a[@href='http://bugs.python.org/issue1000']", "issue 1000"), |
| (".//a[@href='http://bugs.python.org/issue1042']", "explicit caption"), |
| ], |
| '_static/statictmpl.html': [ |
| (".//project", 'Sphinx <Tests>'), |
| ], |
| 'genindex.html': [ |
| # index entries |
| (".//a/strong", "Main"), |
| (".//a/strong", "[1]"), |
| (".//a/strong", "Other"), |
| (".//a", "entry"), |
| (".//dt/a", "double"), |
| ] |
| } |
| |
| if pygments: |
| HTML_XPATH['includes.html'].extend([ |
| (".//pre/span[@class='s']", u'üöä'), |
| (".//div[@class='inc-pyobj1 highlight-text']//pre", |
| r'^class Foo:\n pass\n\s*$'), |
| (".//div[@class='inc-pyobj2 highlight-text']//pre", |
| r'^ def baz\(\):\n pass\n\s*$'), |
| (".//div[@class='inc-lines highlight-text']//pre", |
| r'^class Foo:\n pass\nclass Bar:\n$'), |
| (".//div[@class='inc-startend highlight-text']//pre", |
| u'^foo = "Including Unicode characters: üöä"\\n$'), |
| (".//div[@class='inc-preappend highlight-text']//pre", |
| r'(?m)^START CODE$'), |
| (".//div[@class='inc-pyobj-dedent highlight-python']//span", |
| r'def'), |
| (".//div[@class='inc-tab3 highlight-text']//pre", |
| r'-| |-'), |
| (".//div[@class='inc-tab8 highlight-python']//pre/span", |
| r'-| |-'), |
| ]) |
| HTML_XPATH['subdir/includes.html'].extend([ |
| (".//pre/span", 'line 1'), |
| (".//pre/span", 'line 2'), |
| ]) |
| |
| class NslessParser(ET.XMLParser): |
| """XMLParser that throws away namespaces in tag names.""" |
| |
| def _fixname(self, key): |
| try: |
| return self._names[key] |
| except KeyError: |
| name = key |
| br = name.find('}') |
| if br > 0: |
| name = name[br+1:] |
| self._names[key] = name = self._fixtext(name) |
| return name |
| |
| |
| def check_xpath(etree, fname, path, check, be_found=True): |
| nodes = list(etree.findall(path)) |
| assert nodes != [], ('did not find any node matching xpath ' |
| '%r in file %s' % (path, fname)) |
| if hasattr(check, '__call__'): |
| check(nodes) |
| elif not check: |
| # only check for node presence |
| pass |
| else: |
| rex = re.compile(check) |
| for node in nodes: |
| if node.text and (bool(rex.search(node.text)) ^ (not be_found)): |
| break |
| else: |
| assert False, ('%r not found in any node matching ' |
| 'path %s in %s: %r' % (check, path, fname, |
| [node.text for node in nodes])) |
| |
| def check_static_entries(outdir): |
| staticdir = outdir / '_static' |
| assert staticdir.isdir() |
| # a file from a directory entry in html_static_path |
| assert (staticdir / 'README').isfile() |
| # a directory from a directory entry in html_static_path |
| assert (staticdir / 'subdir' / 'foo.css').isfile() |
| # a file from a file entry in html_static_path |
| assert (staticdir / 'templated.css').isfile() |
| assert (staticdir / 'templated.css').text().splitlines()[1] == __version__ |
| # a file from _static, but matches exclude_patterns |
| assert not (staticdir / 'excluded.css').exists() |
| |
| def check_extra_entries(outdir): |
| assert (outdir / 'robots.txt').isfile() |
| |
| @gen_with_app(buildername='html', warning=html_warnfile, cleanenv=True, |
| confoverrides={'html_context.hckey_co': 'hcval_co'}, |
| tags=['testtag']) |
| def test_html(app): |
| app.builder.build_all() |
| html_warnings = html_warnfile.getvalue().replace(os.sep, '/') |
| html_warnings_exp = HTML_WARNINGS % { |
| 'root': re.escape(app.srcdir.replace(os.sep, '/'))} |
| assert re.match(html_warnings_exp + '$', html_warnings), \ |
| 'Warnings don\'t match:\n' + \ |
| '--- Expected (regex):\n' + html_warnings_exp + \ |
| '--- Got:\n' + html_warnings |
| |
| for fname, paths in iteritems(HTML_XPATH): |
| parser = NslessParser() |
| parser.entity.update(html_entities.entitydefs) |
| fp = open(os.path.join(app.outdir, fname), 'rb') |
| try: |
| etree = ET.parse(fp, parser) |
| finally: |
| fp.close() |
| for path, check in paths: |
| yield check_xpath, etree, fname, path, check |
| |
| check_static_entries(app.builder.outdir) |
| check_extra_entries(app.builder.outdir) |
| |
| @with_app(buildername='html', srcdir='(empty)', |
| confoverrides={'html_sidebars': {'*': ['globaltoc.html']}}, |
| ) |
| def test_html_with_globaltoc_and_hidden_toctree(app): |
| # issue #1157: combination of 'globaltoc.html' and hidden toctree cause |
| # exception. |
| (app.srcdir / 'contents.rst').write_text( |
| '\n.. toctree::' |
| '\n' |
| '\n.. toctree::' |
| '\n :hidden:' |
| '\n') |
| app.builder.build_all() |
| |
| |
| @gen_with_app(buildername='html', srcdir=(test_roots / 'test-tocdepth')) |
| def test_tocdepth(app): |
| # issue #1251 |
| app.builder.build_all() |
| |
| expects = { |
| 'index.html': [ |
| (".//li[@class='toctree-l3']/a", '1.1.1. Foo A1', True), |
| (".//li[@class='toctree-l3']/a", '1.2.1. Foo B1', True), |
| (".//li[@class='toctree-l3']/a", '2.1.1. Bar A1', False), |
| (".//li[@class='toctree-l3']/a", '2.2.1. Bar B1', False), |
| ], |
| 'foo.html': [ |
| (".//h1", '1. Foo', True), |
| (".//h2", '1.1. Foo A', True), |
| (".//h3", '1.1.1. Foo A1', True), |
| (".//h2", '1.2. Foo B', True), |
| (".//h3", '1.2.1. Foo B1', True), |
| ], |
| 'bar.html': [ |
| (".//h1", '2. Bar', True), |
| (".//h2", '2.1. Bar A', True), |
| (".//h2", '2.2. Bar B', True), |
| (".//h3", '2.2.1. Bar B1', True), |
| ], |
| 'baz.html': [ |
| (".//h1", '2.1.1. Baz A', True), |
| ], |
| } |
| |
| for fname, paths in iteritems(expects): |
| parser = NslessParser() |
| parser.entity.update(html_entities.entitydefs) |
| fp = open(os.path.join(app.outdir, fname), 'rb') |
| try: |
| etree = ET.parse(fp, parser) |
| finally: |
| fp.close() |
| |
| for xpath, check, be_found in paths: |
| yield check_xpath, etree, fname, xpath, check, be_found |
| |
| |
| @gen_with_app(buildername='singlehtml', srcdir=(test_roots / 'test-tocdepth')) |
| def test_tocdepth_singlehtml(app): |
| app.builder.build_all() |
| |
| expects = { |
| 'index.html': [ |
| (".//li[@class='toctree-l3']/a", '1.1.1. Foo A1', True), |
| (".//li[@class='toctree-l3']/a", '1.2.1. Foo B1', True), |
| (".//li[@class='toctree-l3']/a", '2.1.1. Bar A1', False), |
| (".//li[@class='toctree-l3']/a", '2.2.1. Bar B1', False), |
| |
| # index.rst |
| (".//h1", 'test-tocdepth', True), |
| |
| # foo.rst |
| (".//h2", '1. Foo', True), |
| (".//h3", '1.1. Foo A', True), |
| (".//h4", '1.1.1. Foo A1', True), |
| (".//h3", '1.2. Foo B', True), |
| (".//h4", '1.2.1. Foo B1', True), |
| |
| # bar.rst |
| (".//h2", '2. Bar', True), |
| (".//h3", '2.1. Bar A', True), |
| (".//h3", '2.2. Bar B', True), |
| (".//h4", '2.2.1. Bar B1', True), |
| |
| # baz.rst |
| (".//h4", '2.1.1. Baz A', True), |
| ], |
| } |
| |
| for fname, paths in iteritems(expects): |
| parser = NslessParser() |
| parser.entity.update(html_entities.entitydefs) |
| fp = open(os.path.join(app.outdir, fname), 'rb') |
| try: |
| etree = ET.parse(fp, parser) |
| finally: |
| fp.close() |
| |
| for xpath, check, be_found in paths: |
| yield check_xpath, etree, fname, xpath, check, be_found |
| |
| |
| @with_app(buildername='html', srcdir='(empty)') |
| def test_url_in_toctree(app): |
| contents = (".. toctree::\n" |
| "\n" |
| " http://sphinx-doc.org/\n" |
| " Latest reference <http://sphinx-doc.org/latest/>\n") |
| |
| (app.srcdir / 'contents.rst').write_text(contents, encoding='utf-8') |
| app.builder.build_all() |
| |
| result = (app.outdir / 'contents.html').text(encoding='utf-8') |
| assert '<a class="reference external" href="http://sphinx-doc.org/">http://sphinx-doc.org/</a>' in result |
| assert '<a class="reference external" href="http://sphinx-doc.org/latest/">Latest reference</a>' in result |