"""Test the HTML builder and check output against XPath."""

import hashlib
import os
import posixpath
import re
from itertools import chain, cycle
from pathlib import Path
from unittest.mock import ANY, call, patch

import pytest
from html5lib import HTMLParser

import sphinx.builders.html
from sphinx.builders.html import validate_html_extra_path, validate_html_static_path
from sphinx.builders.html._assets import _file_checksum
from sphinx.errors import ConfigError, ThemeError
from sphinx.testing.util import strip_escseq
from sphinx.util.inventory import InventoryFile

FIGURE_CAPTION = ".//figure/figcaption/p"


ENV_WARNINGS = """\
%(root)s/autodoc_fodder.py:docstring of autodoc_fodder.MarkupError:\\d+: \
WARNING: Explicit markup ends without a blank line; unexpected unindent.
%(root)s/index.rst:\\d+: WARNING: Encoding 'utf-8-sig' used for reading included \
file '%(root)s/wrongenc.inc' seems to be wrong, try giving an :encoding: option
%(root)s/index.rst:\\d+: WARNING: invalid single index entry ''
%(root)s/index.rst:\\d+: WARNING: image file not readable: foo.png
%(root)s/index.rst:\\d+: WARNING: download file not readable: %(root)s/nonexisting.png
%(root)s/undecodable.rst:\\d+: WARNING: undecodable source characters, replacing \
with "\\?": b?'here: >>>(\\\\|/)xbb<<<((\\\\|/)r)?'
"""

HTML_WARNINGS = ENV_WARNINGS + """\
%(root)s/index.rst:\\d+: WARNING: unknown option: '&option'
%(root)s/index.rst:\\d+: WARNING: citation not found: missing
%(root)s/index.rst:\\d+: WARNING: a suitable image for html builder not found: foo.\\*
%(root)s/index.rst:\\d+: WARNING: Lexing literal_block ".*" as "c" resulted in an error at token: ".*". Retrying in relaxed mode.
"""


etree_cache = {}


@pytest.fixture(scope='module')
def cached_etree_parse():
    def parse(fname):
        if fname in etree_cache:
            return etree_cache[fname]
        with (fname).open('rb') as fp:
            etree = HTMLParser(namespaceHTMLElements=False).parse(fp)
            etree_cache.clear()
            etree_cache[fname] = etree
            return etree
    yield parse
    etree_cache.clear()


def flat_dict(d):
    return chain.from_iterable(
        [
            zip(cycle([fname]), values)
            for fname, values in d.items()
        ],
    )


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
        msg = f'{check!r} not found in tail of any nodes {nodes}'
        raise AssertionError(msg)
    return checker


def check_xpath(etree, fname, path, check, be_found=True):
    nodes = list(etree.findall(path))
    if check is None:
        assert nodes == [], ('found any nodes matching xpath '
                             '%r in file %s' % (path, fname))
        return
    else:
        assert nodes != [], ('did not find any node matching xpath '
                             '%r in file %s' % (path, fname))
    if callable(check):
        check(nodes)
    elif not check:
        # only check for node presence
        pass
    else:
        def get_text(node):
            if node.text is not None:
                # the node has only one text
                return node.text
            else:
                # the node has tags and text; gather texts just under the node
                return ''.join(n.tail or '' for n in node)

        rex = re.compile(check)
        if be_found:
            if any(rex.search(get_text(node)) for node in nodes):
                return
        else:
            if all(not rex.search(get_text(node)) for node in nodes):
                return

        raise AssertionError('%r not found in any node matching '
                             'path %s in %s: %r' % (check, path, fname,
                                                    [node.text for node in nodes]))


@pytest.mark.sphinx('html', testroot='warnings')
def test_html_warnings(app, warning):
    app.build()
    html_warnings = strip_escseq(re.sub(re.escape(os.sep) + '{1,2}', '/', warning.getvalue()))
    html_warnings_exp = HTML_WARNINGS % {
        'root': re.escape(app.srcdir.as_posix())}
    assert re.match(html_warnings_exp + '$', html_warnings), \
        "Warnings don't match:\n" + \
        '--- Expected (regex):\n' + html_warnings_exp + \
        '--- Got:\n' + html_warnings


def test_html4_error(make_app, tmp_path):
    (tmp_path / 'conf.py').write_text('', encoding='utf-8')
    with pytest.raises(
        ConfigError,
        match='HTML 4 is no longer supported by Sphinx',
    ):
        make_app(
            buildername='html',
            srcdir=tmp_path,
            confoverrides={'html4_writer': True},
        )


@pytest.mark.parametrize(("fname", "expect"), flat_dict({
    'images.html': [
        (".//img[@src='_images/img.png']", ''),
        (".//img[@src='_images/img1.png']", ''),
        (".//img[@src='_images/simg.png']", ''),
        (".//img[@src='_images/svgimg.svg']", ''),
        (".//a[@href='_sources/images.txt']", ''),
    ],
    'subdir/images.html': [
        (".//img[@src='../_images/img1.png']", ''),
        (".//img[@src='../_images/rimg.png']", ''),
    ],
    'subdir/includes.html': [
        (".//a[@class='reference download internal']", ''),
        (".//img[@src='../_images/img.png']", ''),
        (".//p", 'This is an include file.'),
        (".//pre/span", 'line 1'),
        (".//pre/span", 'line 2'),
    ],
    'includes.html': [
        (".//pre", 'Max Strauß'),
        (".//a[@class='reference download internal']", ''),
        (".//pre/span", '"quotes"'),
        (".//pre/span", "'included'"),
        (".//pre/span[@class='s2']", 'üöä'),
        (".//div[@class='inc-pyobj1 highlight-text notranslate']//pre",
         r'^class Foo:\n    pass\n\s*$'),
        (".//div[@class='inc-pyobj2 highlight-text notranslate']//pre",
         r'^    def baz\(\):\n        pass\n\s*$'),
        (".//div[@class='inc-lines highlight-text notranslate']//pre",
         r'^class Foo:\n    pass\nclass Bar:\n$'),
        (".//div[@class='inc-startend highlight-text notranslate']//pre",
         '^foo = "Including Unicode characters: üöä"\\n$'),
        (".//div[@class='inc-preappend highlight-text notranslate']//pre",
         r'(?m)^START CODE$'),
        (".//div[@class='inc-pyobj-dedent highlight-python notranslate']//span",
         r'def'),
        (".//div[@class='inc-tab3 highlight-text notranslate']//pre",
         r'-| |-'),
        (".//div[@class='inc-tab8 highlight-python notranslate']//pre/span",
         r'-|      |-'),
    ],
    'autodoc.html': [
        (".//dl[@class='py class']/dt[@id='autodoc_target.Class']", ''),
        (".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span/span", r'\*\*'),
        (".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span/span", r'kwds'),
        (".//dd/p", r'Return spam\.'),
    ],
    'extapi.html': [
        (".//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='admonition-title']", 'My Admonition'),
        (".//div[@class='admonition note']/p", 'Note text.'),
        (".//div[@class='admonition warning']/p", 'Warning text.'),
        # inline markup
        (".//li/p/strong", r'^command\\n$'),
        (".//li/p/strong", r'^program\\n$'),
        (".//li/p/em", r'^dfn\\n$'),
        (".//li/p/kbd", r'^kbd\\n$'),
        (".//li/p/span", 'File \N{TRIANGULAR BULLET} Close'),
        (".//li/p/code/span[@class='pre']", '^a/$'),
        (".//li/p/code/em/span[@class='pre']", '^varpart$'),
        (".//li/p/code/em/span[@class='pre']", '^i$'),
        (".//a[@href='https://peps.python.org/pep-0008/']"
         "[@class='pep reference external']/strong", 'PEP 8'),
        (".//a[@href='https://peps.python.org/pep-0008/']"
         "[@class='pep reference external']/strong",
         'Python Enhancement Proposal #8'),
        (".//a[@href='https://datatracker.ietf.org/doc/html/rfc1.html']"
         "[@class='rfc reference external']/strong", 'RFC 1'),
        (".//a[@href='https://datatracker.ietf.org/doc/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='#some-label'][@class='reference internal']/span", '^here$'),
        (".//a[@href='#some-label'][@class='reference internal']/span", '^there$'),
        (".//a[@href='subdir/includes.html']"
         "[@class='reference internal']/span", 'Including in subdir'),
        (".//a[@href='objects.html#cmdoption-python-c']"
         "[@class='reference internal']/code/span[@class='pre']", '-c'),
        # 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 brackets']", r'1'),
        # created by reference lookup
        (".//a[@href='index.html#ref1']", ''),
        # ``seealso`` directive
        (".//div/p[@class='admonition-title']", 'See also'),
        # a ``hlist`` directive
        (".//table[@class='hlist']/tbody/tr/td/ul/li/p", '^This$'),
        # a ``centered`` directive
        (".//p[@class='centered']/strong", 'LICENSE'),
        # a glossary
        (".//dl/dt[@id='term-boson']", 'boson'),
        (".//dl/dt[@id='term-boson']/a", '¶'),
        # 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'),
        # tests for ``any`` role
        (".//a[@href='#with']/span", 'headings'),
        (".//a[@href='objects.html#func_without_body']/code/span", 'objects'),
        # tests for numeric labels
        (".//a[@href='#id1'][@class='reference internal']/span", 'Testing various markup'),
        # tests for smartypants
        (".//li/p", 'Smart “quotes” in English ‘text’.'),
        (".//li/p", 'Smart — long and – short dashes.'),
        (".//li/p", 'Ellipsis…'),
        (".//li/p/code/span[@class='pre']", 'foo--"bar"...'),
        (".//p", 'Этот «абзац» должен использовать „русские“ кавычки.'),
        (".//p", 'Il dit : « C’est “super” ! »'),
    ],
    'objects.html': [
        (".//dt[@id='mod.Cls.meth1']", ''),
        (".//dt[@id='errmod.Error']", ''),
        (".//dt/span[@class='sig-name descname']/span[@class='pre']", r'long\(parameter,'),
        (".//dt/span[@class='sig-name descname']/span[@class='pre']", r'list\)'),
        (".//dt/span[@class='sig-name descname']/span[@class='pre']", 'another'),
        (".//dt/span[@class='sig-name descname']/span[@class='pre']", 'one'),
        (".//a[@href='#mod.Cls'][@class='reference internal']", ''),
        (".//dl[@class='std userdesc']", ''),
        (".//dt[@id='userdesc-myobj']", ''),
        (".//a[@href='#userdesc-myobj'][@class='reference internal']", ''),
        # docfields
        (".//a[@class='reference internal'][@href='#TimeInt']/em", 'TimeInt'),
        (".//a[@class='reference internal'][@href='#Time']", 'Time'),
        (".//a[@class='reference internal'][@href='#errmod.Error']/strong", 'Error'),
        # 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
        (".//dt[@class='field-odd']", 'Field_name'),
        (".//dt[@class='field-even']", 'Field_name all lower'),
        (".//dt[@class='field-odd']", 'FIELD_NAME'),
        (".//dt[@class='field-even']", 'FIELD_NAME ALL CAPS'),
        (".//dt[@class='field-odd']", 'Field_Name'),
        (".//dt[@class='field-even']", 'Field_Name All Word Caps'),
        (".//dt[@class='field-odd']", 'Field_name'),
        (".//dt[@class='field-even']", 'Field_name First word cap'),
        (".//dt[@class='field-odd']", 'FIELd_name'),
        (".//dt[@class='field-even']", 'FIELd_name PARTial caps'),
        # custom sidebar
        (".//h4", 'Custom sidebar'),
        # docfields
        (".//dd[@class='field-odd']/p/strong", '^moo$'),
        (".//dd[@class='field-odd']/p/strong", tail_check(r'\(Moo\) .* Moo')),
        (".//dd[@class='field-odd']/ul/li/p/strong", '^hour$'),
        (".//dd[@class='field-odd']/ul/li/p/em", '^DuplicateType$'),
        (".//dd[@class='field-odd']/ul/li/p/em", tail_check(r'.* Some parameter')),
        # others
        (".//a[@class='reference internal'][@href='#cmdoption-perl-arg-p']/code/span",
         'perl'),
        (".//a[@class='reference internal'][@href='#cmdoption-perl-arg-p']/code/span",
         '\\+p'),
        (".//a[@class='reference internal'][@href='#cmdoption-perl-ObjC']/code/span",
         '--ObjC\\+\\+'),
        (".//a[@class='reference internal'][@href='#cmdoption-perl-plugin.option']/code/span",
         '--plugin.option'),
        (".//a[@class='reference internal'][@href='#cmdoption-perl-arg-create-auth-token']"
         "/code/span",
         'create-auth-token'),
        (".//a[@class='reference internal'][@href='#cmdoption-perl-arg-arg']/code/span",
         'arg'),
        (".//a[@class='reference internal'][@href='#cmdoption-perl-j']/code/span",
         '-j'),
        (".//a[@class='reference internal'][@href='#cmdoption-hg-arg-commit']/code/span",
         'hg'),
        (".//a[@class='reference internal'][@href='#cmdoption-hg-arg-commit']/code/span",
         'commit'),
        (".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span",
         'git'),
        (".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span",
         'commit'),
        (".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span",
         '-p'),
    ],
    'index.html': [
        (".//meta[@name='hc'][@content='hcval']", ''),
        (".//meta[@name='hc_co'][@content='hcval_co']", ''),
        (".//li[@class='toctree-l1']/a", 'Testing various markup'),
        (".//li[@class='toctree-l2']/a", 'Inline markup'),
        (".//title", 'Sphinx <Tests>'),
        (".//div[@class='footer']", 'copyright text credits'),
        (".//a[@href='http://python.org/']"
         "[@class='reference external']", ''),
        (".//li/p/a[@href='genindex.html']/span", 'Index'),
        (".//li/p/a[@href='py-modindex.html']/span", 'Module Index'),
        # custom sidebar only for contents
        (".//h4", 'Contents sidebar'),
        # custom JavaScript
        (".//script[@src='file://moo.js']", ''),
        # URL in contents
        (".//a[@class='reference external'][@href='http://sphinx-doc.org/']",
         'http://sphinx-doc.org/'),
        (".//a[@class='reference external'][@href='http://sphinx-doc.org/latest/']",
         'Latest reference'),
        # Indirect hyperlink targets across files
        (".//a[@href='markup.html#some-label'][@class='reference internal']/span",
         '^indirect hyperref$'),
    ],
    '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"),
    ],
    'genindex.html': [
        # index entries
        (".//a/strong", "Main"),
        (".//a/strong", "[1]"),
        (".//a/strong", "Other"),
        (".//a", "entry"),
        (".//li/a", "double"),
    ],
    'otherext.html': [
        (".//h1", "Generated section"),
        (".//a[@href='_sources/otherext.foo.txt']", ''),
    ],
}))
@pytest.mark.sphinx('html', tags=['testtag'],
                    confoverrides={'html_context.hckey_co': 'hcval_co'})
@pytest.mark.test_params(shared_result='test_build_html_output')
def test_html5_output(app, cached_etree_parse, fname, expect):
    app.build()
    print(app.outdir / fname)
    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)


@pytest.mark.parametrize(("fname", "expect"), flat_dict({
    'index.html': [
        (".//div[@class='citation']/span", r'Ref1'),
        (".//div[@class='citation']/span", r'Ref_1'),
    ],
    'footnote.html': [
        (".//a[@class='footnote-reference brackets'][@href='#id9'][@id='id1']", r"1"),
        (".//a[@class='footnote-reference brackets'][@href='#id10'][@id='id2']", r"2"),
        (".//a[@class='footnote-reference brackets'][@href='#foo'][@id='id3']", r"3"),
        (".//a[@class='reference internal'][@href='#bar'][@id='id4']/span", r"\[bar\]"),
        (".//a[@class='reference internal'][@href='#baz-qux'][@id='id5']/span", r"\[baz_qux\]"),
        (".//a[@class='footnote-reference brackets'][@href='#id11'][@id='id6']", r"4"),
        (".//a[@class='footnote-reference brackets'][@href='#id12'][@id='id7']", r"5"),
        (".//aside[@class='footnote brackets']/span/a[@href='#id1']", r"1"),
        (".//aside[@class='footnote brackets']/span/a[@href='#id2']", r"2"),
        (".//aside[@class='footnote brackets']/span/a[@href='#id3']", r"3"),
        (".//div[@class='citation']/span/a[@href='#id4']", r"bar"),
        (".//div[@class='citation']/span/a[@href='#id5']", r"baz_qux"),
        (".//aside[@class='footnote brackets']/span/a[@href='#id6']", r"4"),
        (".//aside[@class='footnote brackets']/span/a[@href='#id7']", r"5"),
        (".//aside[@class='footnote brackets']/span/a[@href='#id8']", r"6"),
    ],
}))
@pytest.mark.sphinx('html')
@pytest.mark.test_params(shared_result='test_build_html_output_docutils18')
def test_docutils_output(app, cached_etree_parse, fname, expect):
    app.build()
    print(app.outdir / fname)
    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)


@pytest.mark.sphinx('html', parallel=2)
def test_html_parallel(app):
    app.build()


@pytest.mark.sphinx('html')
@pytest.mark.test_params(shared_result='test_build_html_output')
def test_html_download(app):
    app.build()

    # subdir/includes.html
    result = (app.outdir / 'subdir' / 'includes.html').read_text(encoding='utf8')
    pattern = ('<a class="reference download internal" download="" '
               'href="../(_downloads/.*/img.png)">')
    matched = re.search(pattern, result)
    assert matched
    assert (app.outdir / matched.group(1)).exists()
    filename = matched.group(1)

    # includes.html
    result = (app.outdir / 'includes.html').read_text(encoding='utf8')
    pattern = ('<a class="reference download internal" download="" '
               'href="(_downloads/.*/img.png)">')
    matched = re.search(pattern, result)
    assert matched
    assert (app.outdir / matched.group(1)).exists()
    assert matched.group(1) == filename

    pattern = ('<a class="reference download internal" download="" '
               'href="(_downloads/.*/)(file_with_special_%23_chars.xyz)">')
    matched = re.search(pattern, result)
    assert matched
    assert (app.outdir / matched.group(1) / "file_with_special_#_chars.xyz").exists()


@pytest.mark.sphinx('html', testroot='roles-download')
def test_html_download_role(app, status, warning):
    app.build()
    digest = hashlib.md5(b'dummy.dat', usedforsecurity=False).hexdigest()
    assert (app.outdir / '_downloads' / digest / 'dummy.dat').exists()
    digest_another = hashlib.md5(b'another/dummy.dat', usedforsecurity=False).hexdigest()
    assert (app.outdir / '_downloads' / digest_another / 'dummy.dat').exists()

    content = (app.outdir / 'index.html').read_text(encoding='utf8')
    assert (('<li><p><a class="reference download internal" download="" '
             'href="_downloads/%s/dummy.dat">'
             '<code class="xref download docutils literal notranslate">'
             '<span class="pre">dummy.dat</span></code></a></p></li>' % digest)
            in content)
    assert (('<li><p><a class="reference download internal" download="" '
             'href="_downloads/%s/dummy.dat">'
             '<code class="xref download docutils literal notranslate">'
             '<span class="pre">another/dummy.dat</span></code></a></p></li>' %
             digest_another) in content)
    assert ('<li><p><code class="xref download docutils literal notranslate">'
            '<span class="pre">not_found.dat</span></code></p></li>' in content)
    assert ('<li><p><a class="reference download external" download="" '
            'href="http://www.sphinx-doc.org/en/master/_static/sphinxheader.png">'
            '<code class="xref download docutils literal notranslate">'
            '<span class="pre">Sphinx</span> <span class="pre">logo</span>'
            '</code></a></p></li>' in content)


@pytest.mark.sphinx('html', testroot='build-html-translator')
def test_html_translator(app):
    app.build()
    assert app.builder.docwriter.visitor.depart_with_node == 10


@pytest.mark.parametrize(("fname", "expect"), flat_dict({
    '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", 'Foo', True),
        (".//h2", 'Foo A', True),
        (".//h3", 'Foo A1', True),
        (".//h2", 'Foo B', True),
        (".//h3", 'Foo B1', True),

        (".//h1//span[@class='section-number']", '1. ', True),
        (".//h2//span[@class='section-number']", '1.1. ', True),
        (".//h3//span[@class='section-number']", '1.1.1. ', True),
        (".//h2//span[@class='section-number']", '1.2. ', True),
        (".//h3//span[@class='section-number']", '1.2.1. ', True),

        (".//div[@class='sphinxsidebarwrapper']//li/a", '1.1. Foo A', True),
        (".//div[@class='sphinxsidebarwrapper']//li/a", '1.1.1. Foo A1', True),
        (".//div[@class='sphinxsidebarwrapper']//li/a", '1.2. Foo B', True),
        (".//div[@class='sphinxsidebarwrapper']//li/a", '1.2.1. Foo B1', True),
    ],
    'bar.html': [
        (".//h1", 'Bar', True),
        (".//h2", 'Bar A', True),
        (".//h2", 'Bar B', True),
        (".//h3", 'Bar B1', True),
        (".//h1//span[@class='section-number']", '2. ', True),
        (".//h2//span[@class='section-number']", '2.1. ', True),
        (".//h2//span[@class='section-number']", '2.2. ', True),
        (".//h3//span[@class='section-number']", '2.2.1. ', True),
        (".//div[@class='sphinxsidebarwrapper']//li/a", '2. Bar', True),
        (".//div[@class='sphinxsidebarwrapper']//li/a", '2.1. Bar A', True),
        (".//div[@class='sphinxsidebarwrapper']//li/a", '2.2. Bar B', True),
        (".//div[@class='sphinxsidebarwrapper']//li/a", '2.2.1. Bar B1', False),
    ],
    'baz.html': [
        (".//h1", 'Baz A', True),
        (".//h1//span[@class='section-number']", '2.1.1. ', True),
    ],
}))
@pytest.mark.sphinx('html', testroot='tocdepth')
@pytest.mark.test_params(shared_result='test_build_html_tocdepth')
def test_tocdepth(app, cached_etree_parse, fname, expect):
    app.build()
    # issue #1251
    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)


@pytest.mark.parametrize(("fname", "expect"), flat_dict({
    '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", 'Foo', True),
        (".//h3", 'Foo A', True),
        (".//h4", 'Foo A1', True),
        (".//h3", 'Foo B', True),
        (".//h4", 'Foo B1', True),
        (".//h2//span[@class='section-number']", '1. ', True),
        (".//h3//span[@class='section-number']", '1.1. ', True),
        (".//h4//span[@class='section-number']", '1.1.1. ', True),
        (".//h3//span[@class='section-number']", '1.2. ', True),
        (".//h4//span[@class='section-number']", '1.2.1. ', True),

        # bar.rst
        (".//h2", 'Bar', True),
        (".//h3", 'Bar A', True),
        (".//h3", 'Bar B', True),
        (".//h4", 'Bar B1', True),
        (".//h2//span[@class='section-number']", '2. ', True),
        (".//h3//span[@class='section-number']", '2.1. ', True),
        (".//h3//span[@class='section-number']", '2.2. ', True),
        (".//h4//span[@class='section-number']", '2.2.1. ', True),

        # baz.rst
        (".//h4", 'Baz A', True),
        (".//h4//span[@class='section-number']", '2.1.1. ', True),
    ],
}))
@pytest.mark.sphinx('singlehtml', testroot='tocdepth')
@pytest.mark.test_params(shared_result='test_build_html_tocdepth')
def test_tocdepth_singlehtml(app, cached_etree_parse, fname, expect):
    app.build()
    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)


@pytest.mark.sphinx('html', testroot='numfig')
@pytest.mark.test_params(shared_result='test_build_html_numfig')
def test_numfig_disabled_warn(app, warning):
    app.build()
    warnings = warning.getvalue()
    assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' in warnings
    assert 'index.rst:56: WARNING: invalid numfig_format: invalid' not in warnings
    assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' not in warnings


@pytest.mark.parametrize(("fname", "expect"), flat_dict({
    'index.html': [
        (FIGURE_CAPTION + "/span[@class='caption-number']", None, True),
        (".//table/caption/span[@class='caption-number']", None, True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", None, True),
        (".//li/p/code/span", '^fig1$', True),
        (".//li/p/code/span", '^Figure%s$', True),
        (".//li/p/code/span", '^table-1$', True),
        (".//li/p/code/span", '^Table:%s$', True),
        (".//li/p/code/span", '^CODE_1$', True),
        (".//li/p/code/span", '^Code-%s$', True),
        (".//li/p/a/span", '^Section 1$', True),
        (".//li/p/a/span", '^Section 2.1$', True),
        (".//li/p/code/span", '^Fig.{number}$', True),
        (".//li/p/a/span", '^Sect.1 Foo$', True),
    ],
    'foo.html': [
        (FIGURE_CAPTION + "/span[@class='caption-number']", None, True),
        (".//table/caption/span[@class='caption-number']", None, True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", None, True),
    ],
    'bar.html': [
        (FIGURE_CAPTION + "/span[@class='caption-number']", None, True),
        (".//table/caption/span[@class='caption-number']", None, True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", None, True),
    ],
    'baz.html': [
        (FIGURE_CAPTION + "/span[@class='caption-number']", None, True),
        (".//table/caption/span[@class='caption-number']", None, True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", None, True),
    ],
}))
@pytest.mark.sphinx('html', testroot='numfig')
@pytest.mark.test_params(shared_result='test_build_html_numfig')
def test_numfig_disabled(app, cached_etree_parse, fname, expect):
    app.build()
    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)


@pytest.mark.sphinx(
    'html', testroot='numfig',
    srcdir='test_numfig_without_numbered_toctree_warn',
    confoverrides={'numfig': True})
def test_numfig_without_numbered_toctree_warn(app, warning):
    app.build()
    # remove :numbered: option
    index = (app.srcdir / 'index.rst').read_text(encoding='utf8')
    index = re.sub(':numbered:.*', '', index)
    (app.srcdir / 'index.rst').write_text(index, encoding='utf8')
    app.build()

    warnings = warning.getvalue()
    assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings
    assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings
    assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings
    assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings


@pytest.mark.parametrize(("fname", "expect"), flat_dict({
    'index.html': [
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 9 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 10 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 9 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 10 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 9 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 10 $', True),
        (".//li/p/a/span", '^Fig. 9$', True),
        (".//li/p/a/span", '^Figure6$', True),
        (".//li/p/a/span", '^Table 9$', True),
        (".//li/p/a/span", '^Table:6$', True),
        (".//li/p/a/span", '^Listing 9$', True),
        (".//li/p/a/span", '^Code-6$', True),
        (".//li/p/code/span", '^foo$', True),
        (".//li/p/code/span", '^bar_a$', True),
        (".//li/p/a/span", '^Fig.9 should be Fig.1$', True),
        (".//li/p/code/span", '^Sect.{number}$', True),
    ],
    'foo.html': [
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 3 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 4 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 1 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 2 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 3 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 4 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 1 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 2 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 3 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 4 $', True),
    ],
    'bar.html': [
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 5 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 7 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 8 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 5 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 7 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 8 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 5 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 7 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 8 $', True),
    ],
    'baz.html': [
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 6 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 6 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 6 $', True),
    ],
}))
@pytest.mark.sphinx(
    'html', testroot='numfig',
    srcdir='test_numfig_without_numbered_toctree',
    confoverrides={'numfig': True})
def test_numfig_without_numbered_toctree(app, cached_etree_parse, fname, expect):
    # remove :numbered: option
    index = (app.srcdir / 'index.rst').read_text(encoding='utf8')
    index = re.sub(':numbered:.*', '', index)
    (app.srcdir / 'index.rst').write_text(index, encoding='utf8')

    if not os.listdir(app.outdir):
        app.build()
    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)


@pytest.mark.sphinx('html', testroot='numfig', confoverrides={'numfig': True})
@pytest.mark.test_params(shared_result='test_build_html_numfig_on')
def test_numfig_with_numbered_toctree_warn(app, warning):
    app.build()
    warnings = warning.getvalue()
    assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings
    assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings
    assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings
    assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings


@pytest.mark.parametrize(("fname", "expect"), flat_dict({
    'index.html': [
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 1 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 2 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 1 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 2 $', True),
        (".//li/p/a/span", '^Fig. 1$', True),
        (".//li/p/a/span", '^Figure2.2$', True),
        (".//li/p/a/span", '^Table 1$', True),
        (".//li/p/a/span", '^Table:2.2$', True),
        (".//li/p/a/span", '^Listing 1$', True),
        (".//li/p/a/span", '^Code-2.2$', True),
        (".//li/p/a/span", '^Section.1$', True),
        (".//li/p/a/span", '^Section.2.1$', True),
        (".//li/p/a/span", '^Fig.1 should be Fig.1$', True),
        (".//li/p/a/span", '^Sect.1 Foo$', True),
    ],
    'foo.html': [
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.2 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.3 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.4 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 1.1 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 1.2 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 1.3 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 1.4 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 1.1 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 1.2 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 1.3 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 1.4 $', True),
    ],
    'bar.html': [
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.3 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.4 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 2.1 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 2.3 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 2.4 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 2.1 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 2.3 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 2.4 $', True),
    ],
    'baz.html': [
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.2 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 2.2 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 2.2 $', True),
    ],
}))
@pytest.mark.sphinx('html', testroot='numfig', confoverrides={'numfig': True})
@pytest.mark.test_params(shared_result='test_build_html_numfig_on')
def test_numfig_with_numbered_toctree(app, cached_etree_parse, fname, expect):
    app.build()
    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)


@pytest.mark.sphinx('html', testroot='numfig', confoverrides={
    'numfig': True,
    'numfig_format': {'figure': 'Figure:%s',
                      'table': 'Tab_%s',
                      'code-block': 'Code-%s',
                      'section': 'SECTION-%s'}})
@pytest.mark.test_params(shared_result='test_build_html_numfig_format_warn')
def test_numfig_with_prefix_warn(app, warning):
    app.build()
    warnings = warning.getvalue()
    assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings
    assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings
    assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings
    assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings


@pytest.mark.parametrize(("fname", "expect"), flat_dict({
    'index.html': [
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Tab_1 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Tab_2 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Code-1 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Code-2 $', True),
        (".//li/p/a/span", '^Figure:1$', True),
        (".//li/p/a/span", '^Figure2.2$', True),
        (".//li/p/a/span", '^Tab_1$', True),
        (".//li/p/a/span", '^Table:2.2$', True),
        (".//li/p/a/span", '^Code-1$', True),
        (".//li/p/a/span", '^Code-2.2$', True),
        (".//li/p/a/span", '^SECTION-1$', True),
        (".//li/p/a/span", '^SECTION-2.1$', True),
        (".//li/p/a/span", '^Fig.1 should be Fig.1$', True),
        (".//li/p/a/span", '^Sect.1 Foo$', True),
    ],
    'foo.html': [
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.1 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.2 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.3 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.4 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Tab_1.1 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Tab_1.2 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Tab_1.3 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Tab_1.4 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Code-1.1 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Code-1.2 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Code-1.3 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Code-1.4 $', True),
    ],
    'bar.html': [
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.1 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.3 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.4 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Tab_2.1 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Tab_2.3 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Tab_2.4 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Code-2.1 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Code-2.3 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Code-2.4 $', True),
    ],
    'baz.html': [
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.2 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Tab_2.2 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Code-2.2 $', True),
    ],
}))
@pytest.mark.sphinx('html', testroot='numfig',
                    confoverrides={'numfig': True,
                                   'numfig_format': {'figure': 'Figure:%s',
                                                     'table': 'Tab_%s',
                                                     'code-block': 'Code-%s',
                                                     'section': 'SECTION-%s'}})
@pytest.mark.test_params(shared_result='test_build_html_numfig_format_warn')
def test_numfig_with_prefix(app, cached_etree_parse, fname, expect):
    app.build()
    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)


@pytest.mark.sphinx('html', testroot='numfig',
                    confoverrides={'numfig': True, 'numfig_secnum_depth': 2})
@pytest.mark.test_params(shared_result='test_build_html_numfig_depth_2')
def test_numfig_with_secnum_depth_warn(app, warning):
    app.build()
    warnings = warning.getvalue()
    assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings
    assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings
    assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings
    assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings


@pytest.mark.parametrize(("fname", "expect"), flat_dict({
    'index.html': [
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 1 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 2 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 1 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 2 $', True),
        (".//li/p/a/span", '^Fig. 1$', True),
        (".//li/p/a/span", '^Figure2.1.2$', True),
        (".//li/p/a/span", '^Table 1$', True),
        (".//li/p/a/span", '^Table:2.1.2$', True),
        (".//li/p/a/span", '^Listing 1$', True),
        (".//li/p/a/span", '^Code-2.1.2$', True),
        (".//li/p/a/span", '^Section.1$', True),
        (".//li/p/a/span", '^Section.2.1$', True),
        (".//li/p/a/span", '^Fig.1 should be Fig.1$', True),
        (".//li/p/a/span", '^Sect.1 Foo$', True),
    ],
    'foo.html': [
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1.1 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1.2 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.2.1 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 1.1 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 1.1.1 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 1.1.2 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 1.2.1 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 1.1 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 1.1.1 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 1.1.2 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 1.2.1 $', True),
    ],
    'bar.html': [
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1.1 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1.3 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.2.1 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 2.1.1 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 2.1.3 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 2.2.1 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 2.1.1 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 2.1.3 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 2.2.1 $', True),
    ],
    'baz.html': [
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1.2 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 2.1.2 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 2.1.2 $', True),
    ],
}))
@pytest.mark.sphinx('html', testroot='numfig',
                    confoverrides={'numfig': True,
                                   'numfig_secnum_depth': 2})
@pytest.mark.test_params(shared_result='test_build_html_numfig_depth_2')
def test_numfig_with_secnum_depth(app, cached_etree_parse, fname, expect):
    app.build()
    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)


@pytest.mark.parametrize(("fname", "expect"), flat_dict({
    'index.html': [
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 1 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 2 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 1 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 2 $', True),
        (".//li/p/a/span", '^Fig. 1$', True),
        (".//li/p/a/span", '^Figure2.2$', True),
        (".//li/p/a/span", '^Table 1$', True),
        (".//li/p/a/span", '^Table:2.2$', True),
        (".//li/p/a/span", '^Listing 1$', True),
        (".//li/p/a/span", '^Code-2.2$', True),
        (".//li/p/a/span", '^Section.1$', True),
        (".//li/p/a/span", '^Section.2.1$', True),
        (".//li/p/a/span", '^Fig.1 should be Fig.1$', True),
        (".//li/p/a/span", '^Sect.1 Foo$', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.2 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.3 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.4 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 1.1 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 1.2 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 1.3 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 1.4 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 1.1 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 1.2 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 1.3 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 1.4 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.3 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.4 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 2.1 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 2.3 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 2.4 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 2.1 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 2.3 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 2.4 $', True),
        (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.2 $', True),
        (".//table/caption/span[@class='caption-number']",
         '^Table 2.2 $', True),
        (".//div[@class='code-block-caption']/"
         "span[@class='caption-number']", '^Listing 2.2 $', True),
    ],
}))
@pytest.mark.sphinx('singlehtml', testroot='numfig', confoverrides={'numfig': True})
@pytest.mark.test_params(shared_result='test_build_html_numfig_on')
def test_numfig_with_singlehtml(app, cached_etree_parse, fname, expect):
    app.build()
    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)


@pytest.mark.parametrize(("fname", "expect"), flat_dict({
    'index.html': [
        (FIGURE_CAPTION + "//span[@class='caption-number']", "Fig. 1", True),
        (FIGURE_CAPTION + "//span[@class='caption-number']", "Fig. 2", True),
        (FIGURE_CAPTION + "//span[@class='caption-number']", "Fig. 3", True),
        (".//div//span[@class='caption-number']", "No.1 ", True),
        (".//div//span[@class='caption-number']", "No.2 ", True),
        (".//li/p/a/span", 'Fig. 1', True),
        (".//li/p/a/span", 'Fig. 2', True),
        (".//li/p/a/span", 'Fig. 3', True),
        (".//li/p/a/span", 'No.1', True),
        (".//li/p/a/span", 'No.2', True),
    ],
}))
@pytest.mark.sphinx('html', testroot='add_enumerable_node',
                    srcdir='test_enumerable_node')
def test_enumerable_node(app, cached_etree_parse, fname, expect):
    app.build()
    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)


@pytest.mark.sphinx('html', testroot='html_assets')
def test_html_assets(app):
    app.builder.build_all()

    # exclude_path and its family
    assert not (app.outdir / 'static' / 'index.html').exists()
    assert not (app.outdir / 'extra' / 'index.html').exists()

    # html_static_path
    assert not (app.outdir / '_static' / '.htaccess').exists()
    assert not (app.outdir / '_static' / '.htpasswd').exists()
    assert (app.outdir / '_static' / 'API.html').exists()
    assert (app.outdir / '_static' / 'API.html').read_text(encoding='utf8') == 'Sphinx-1.4.4'
    assert (app.outdir / '_static' / 'css' / 'style.css').exists()
    assert (app.outdir / '_static' / 'js' / 'custom.js').exists()
    assert (app.outdir / '_static' / 'rimg.png').exists()
    assert not (app.outdir / '_static' / '_build' / 'index.html').exists()
    assert (app.outdir / '_static' / 'background.png').exists()
    assert not (app.outdir / '_static' / 'subdir' / '.htaccess').exists()
    assert not (app.outdir / '_static' / 'subdir' / '.htpasswd').exists()

    # html_extra_path
    assert (app.outdir / '.htaccess').exists()
    assert not (app.outdir / '.htpasswd').exists()
    assert (app.outdir / 'API.html_t').exists()
    assert (app.outdir / 'css/style.css').exists()
    assert (app.outdir / 'rimg.png').exists()
    assert not (app.outdir / '_build' / 'index.html').exists()
    assert (app.outdir / 'background.png').exists()
    assert (app.outdir / 'subdir' / '.htaccess').exists()
    assert not (app.outdir / 'subdir' / '.htpasswd').exists()

    # html_css_files
    content = (app.outdir / 'index.html').read_text(encoding='utf8')
    assert '<link rel="stylesheet" type="text/css" href="_static/css/style.css" />' in content
    assert ('<link media="print" rel="stylesheet" title="title" type="text/css" '
            'href="https://example.com/custom.css" />' in content)

    # html_js_files
    assert '<script src="_static/js/custom.js"></script>' in content
    assert ('<script async="async" src="https://example.com/script.js">'
            '</script>' in content)


@pytest.mark.sphinx('html', testroot='html_assets')
def test_assets_order(app, monkeypatch):
    monkeypatch.setattr(sphinx.builders.html, '_file_checksum', lambda o, f: '')

    app.add_css_file('normal.css')
    app.add_css_file('early.css', priority=100)
    app.add_css_file('late.css', priority=750)
    app.add_css_file('lazy.css', priority=900)
    app.add_js_file('normal.js')
    app.add_js_file('early.js', priority=100)
    app.add_js_file('late.js', priority=750)
    app.add_js_file('lazy.js', priority=900)

    app.builder.build_all()
    content = (app.outdir / 'index.html').read_text(encoding='utf8')

    # css_files
    expected = [
        '_static/early.css',
        '_static/pygments.css',
        '_static/alabaster.css',
        'https://example.com/custom.css',
        '_static/normal.css',
        '_static/late.css',
        '_static/css/style.css',
        '_static/lazy.css',
    ]
    pattern = '.*'.join(f'href="{re.escape(f)}"' for f in expected)
    assert re.search(pattern, content, re.DOTALL), content

    # js_files
    expected = [
        '_static/early.js',
        '_static/doctools.js',
        '_static/sphinx_highlight.js',
        'https://example.com/script.js',
        '_static/normal.js',
        '_static/late.js',
        '_static/js/custom.js',
        '_static/lazy.js',
    ]
    pattern = '.*'.join(f'src="{re.escape(f)}"' for f in expected)
    assert re.search(pattern, content, re.DOTALL), content


@pytest.mark.sphinx('html', testroot='html_file_checksum')
def test_file_checksum(app):
    app.add_css_file('stylesheet-a.css')
    app.add_css_file('stylesheet-b.css')
    app.add_css_file('https://example.com/custom.css')
    app.add_js_file('script.js')
    app.add_js_file('empty.js')
    app.add_js_file('https://example.com/script.js')

    app.builder.build_all()
    content = (app.outdir / 'index.html').read_text(encoding='utf8')

    # checksum for local files
    assert '<link rel="stylesheet" type="text/css" href="_static/stylesheet-a.css?v=e575b6df" />' in content
    assert '<link rel="stylesheet" type="text/css" href="_static/stylesheet-b.css?v=a2d5cc0f" />' in content
    assert '<script src="_static/script.js?v=48278d48"></script>' in content

    # empty files have no checksum
    assert '<script src="_static/empty.js"></script>' in content

    # no checksum for hyperlinks
    assert '<link rel="stylesheet" type="text/css" href="https://example.com/custom.css" />' in content
    assert '<script src="https://example.com/script.js"></script>' in content


def test_file_checksum_query_string():
    with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'):
        _file_checksum(Path(), 'with_query_string.css?dead_parrots=1')

    with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'):
        _file_checksum(Path(), 'with_query_string.js?dead_parrots=1')

    with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'):
        _file_checksum(Path.cwd(), '_static/with_query_string.css?dead_parrots=1')

    with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'):
        _file_checksum(Path.cwd(), '_static/with_query_string.js?dead_parrots=1')


@pytest.mark.sphinx('html', testroot='html_assets')
def test_javscript_loading_method(app):
    app.add_js_file('normal.js')
    app.add_js_file('early.js', loading_method='async')
    app.add_js_file('late.js', loading_method='defer')

    app.builder.build_all()
    content = (app.outdir / 'index.html').read_text(encoding='utf8')

    assert '<script src="_static/normal.js"></script>' in content
    assert '<script async="async" src="_static/early.js"></script>' in content
    assert '<script defer="defer" src="_static/late.js"></script>' in content


@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_copy_source': False})
def test_html_copy_source(app):
    app.builder.build_all()
    assert not (app.outdir / '_sources' / 'index.rst.txt').exists()


@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_sourcelink_suffix': '.txt'})
def test_html_sourcelink_suffix(app):
    app.builder.build_all()
    assert (app.outdir / '_sources' / 'index.rst.txt').exists()


@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_sourcelink_suffix': '.rst'})
def test_html_sourcelink_suffix_same(app):
    app.builder.build_all()
    assert (app.outdir / '_sources' / 'index.rst').exists()


@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_sourcelink_suffix': ''})
def test_html_sourcelink_suffix_empty(app):
    app.builder.build_all()
    assert (app.outdir / '_sources' / 'index.rst').exists()


@pytest.mark.sphinx('html', testroot='html_entity')
def test_html_entity(app):
    app.builder.build_all()
    valid_entities = {'amp', 'lt', 'gt', 'quot', 'apos'}
    content = (app.outdir / 'index.html').read_text(encoding='utf8')
    for entity in re.findall(r'&([a-z]+);', content, re.M):
        assert entity not in valid_entities


@pytest.mark.sphinx('html', testroot='basic')
def test_html_inventory(app):
    app.builder.build_all()

    with app.outdir.joinpath('objects.inv').open('rb') as f:
        invdata = InventoryFile.load(f, 'https://www.google.com', posixpath.join)

    assert set(invdata.keys()) == {'std:label', 'std:doc'}
    assert set(invdata['std:label'].keys()) == {'modindex',
                                                'py-modindex',
                                                'genindex',
                                                'search'}
    assert invdata['std:label']['modindex'] == ('Python',
                                                '',
                                                'https://www.google.com/py-modindex.html',
                                                'Module Index')
    assert invdata['std:label']['py-modindex'] == ('Python',
                                                   '',
                                                   'https://www.google.com/py-modindex.html',
                                                   'Python Module Index')
    assert invdata['std:label']['genindex'] == ('Python',
                                                '',
                                                'https://www.google.com/genindex.html',
                                                'Index')
    assert invdata['std:label']['search'] == ('Python',
                                              '',
                                              'https://www.google.com/search.html',
                                              'Search Page')
    assert set(invdata['std:doc'].keys()) == {'index'}
    assert invdata['std:doc']['index'] == ('Python',
                                           '',
                                           'https://www.google.com/index.html',
                                           'The basic Sphinx documentation for testing')


@pytest.mark.sphinx('html', testroot='images', confoverrides={'html_sourcelink_suffix': ''})
def test_html_anchor_for_figure(app):
    app.builder.build_all()
    content = (app.outdir / 'index.html').read_text(encoding='utf8')
    assert ('<figcaption>\n<p><span class="caption-text">The caption of pic</span>'
            '<a class="headerlink" href="#id1" title="Link to this image">¶</a></p>\n</figcaption>'
            in content)


@pytest.mark.sphinx('html', testroot='directives-raw')
def test_html_raw_directive(app, status, warning):
    app.builder.build_all()
    result = (app.outdir / 'index.html').read_text(encoding='utf8')

    # standard case
    assert 'standalone raw directive (HTML)' in result
    assert 'standalone raw directive (LaTeX)' not in result

    # with substitution
    assert '<p>HTML: abc def ghi</p>' in result
    assert '<p>LaTeX: abc  ghi</p>' in result


@pytest.mark.parametrize(("fname", "expect"), flat_dict({
    'index.html': [
        (".//link[@href='_static/persistent.css']"
         "[@rel='stylesheet']", '', True),
        (".//link[@href='_static/default.css']"
         "[@rel='stylesheet']"
         "[@title='Default']", '', True),
        (".//link[@href='_static/alternate1.css']"
         "[@rel='alternate stylesheet']"
         "[@title='Alternate']", '', True),
        (".//link[@href='_static/alternate2.css']"
         "[@rel='alternate stylesheet']", '', True),
        (".//link[@href='_static/more_persistent.css']"
         "[@rel='stylesheet']", '', True),
        (".//link[@href='_static/more_default.css']"
         "[@rel='stylesheet']"
         "[@title='Default']", '', True),
        (".//link[@href='_static/more_alternate1.css']"
         "[@rel='alternate stylesheet']"
         "[@title='Alternate']", '', True),
        (".//link[@href='_static/more_alternate2.css']"
         "[@rel='alternate stylesheet']", '', True),
    ],
}))
@pytest.mark.sphinx('html', testroot='stylesheets')
def test_alternate_stylesheets(app, cached_etree_parse, fname, expect):
    app.build()
    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)


@pytest.mark.sphinx('html', testroot='html_style')
def test_html_style(app, status, warning):
    app.build()
    result = (app.outdir / 'index.html').read_text(encoding='utf8')
    assert '<link rel="stylesheet" type="text/css" href="_static/default.css" />' in result
    assert ('<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />'
            not in result)


@pytest.mark.sphinx('html', testroot='images')
def test_html_remote_images(app, status, warning):
    app.builder.build_all()

    result = (app.outdir / 'index.html').read_text(encoding='utf8')
    assert ('<img alt="https://www.python.org/static/img/python-logo.png" '
            'src="https://www.python.org/static/img/python-logo.png" />' in result)
    assert not (app.outdir / 'python-logo.png').exists()


@pytest.mark.sphinx('html', testroot='image-escape')
def test_html_encoded_image(app, status, warning):
    app.builder.build_all()

    result = (app.outdir / 'index.html').read_text(encoding='utf8')
    assert ('<img alt="_images/img_%231.png" src="_images/img_%231.png" />' in result)
    assert (app.outdir / '_images/img_#1.png').exists()


@pytest.mark.sphinx('html', testroot='remote-logo')
def test_html_remote_logo(app, status, warning):
    app.builder.build_all()

    result = (app.outdir / 'index.html').read_text(encoding='utf8')
    assert ('<img class="logo" src="https://www.python.org/static/img/python-logo.png" alt="Logo"/>' in result)
    assert ('<link rel="icon" href="https://www.python.org/static/favicon.ico"/>' in result)
    assert not (app.outdir / 'python-logo.png').exists()


@pytest.mark.sphinx('html', testroot='local-logo')
def test_html_local_logo(app, status, warning):
    app.builder.build_all()

    result = (app.outdir / 'index.html').read_text(encoding='utf8')
    assert ('<img class="logo" src="_static/img.png" alt="Logo"/>' in result)
    assert (app.outdir / '_static/img.png').exists()


@pytest.mark.sphinx('html', testroot='basic')
def test_html_sidebar(app, status, warning):
    ctx = {}

    # default for alabaster
    app.builder.build_all()
    result = (app.outdir / 'index.html').read_text(encoding='utf8')
    assert ('<div class="sphinxsidebar" role="navigation" '
            'aria-label="main navigation">' in result)
    assert '<h1 class="logo"><a href="#">Python</a></h1>' in result
    assert '<h3>Navigation</h3>' in result
    assert '<h3>Related Topics</h3>' in result
    assert '<h3 id="searchlabel">Quick search</h3>' in result

    app.builder.add_sidebars('index', ctx)
    assert ctx['sidebars'] == ['about.html', 'navigation.html', 'relations.html',
                               'searchbox.html', 'donate.html']

    # only relations.html
    app.config.html_sidebars = {'**': ['relations.html']}
    app.builder.build_all()
    result = (app.outdir / 'index.html').read_text(encoding='utf8')
    assert ('<div class="sphinxsidebar" role="navigation" '
            'aria-label="main navigation">' in result)
    assert '<h1 class="logo"><a href="#">Python</a></h1>' not in result
    assert '<h3>Navigation</h3>' not in result
    assert '<h3>Related Topics</h3>' in result
    assert '<h3 id="searchlabel">Quick search</h3>' not in result

    app.builder.add_sidebars('index', ctx)
    assert ctx['sidebars'] == ['relations.html']

    # no sidebars
    app.config.html_sidebars = {'**': []}
    app.builder.build_all()
    result = (app.outdir / 'index.html').read_text(encoding='utf8')
    assert ('<div class="sphinxsidebar" role="navigation" '
            'aria-label="main navigation">' not in result)
    assert '<h1 class="logo"><a href="#">Python</a></h1>' not in result
    assert '<h3>Navigation</h3>' not in result
    assert '<h3>Related Topics</h3>' not in result
    assert '<h3 id="searchlabel">Quick search</h3>' not in result

    app.builder.add_sidebars('index', ctx)
    assert ctx['sidebars'] == []


@pytest.mark.parametrize(("fname", "expect"), flat_dict({
    'index.html': [(".//em/a[@href='https://example.com/man.1']", "", True),
                   (".//em/a[@href='https://example.com/ls.1']", "", True),
                   (".//em/a[@href='https://example.com/sphinx.']", "", True)],

}))
@pytest.mark.sphinx('html', testroot='manpage_url', confoverrides={
    'manpages_url': 'https://example.com/{page}.{section}'})
@pytest.mark.test_params(shared_result='test_build_html_manpage_url')
def test_html_manpage(app, cached_etree_parse, fname, expect):
    app.build()
    check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)


@pytest.mark.sphinx('html', testroot='toctree-glob',
                    confoverrides={'html_baseurl': 'https://example.com/'})
def test_html_baseurl(app, status, warning):
    app.build()

    result = (app.outdir / 'index.html').read_text(encoding='utf8')
    assert '<link rel="canonical" href="https://example.com/index.html" />' in result

    result = (app.outdir / 'qux' / 'index.html').read_text(encoding='utf8')
    assert '<link rel="canonical" href="https://example.com/qux/index.html" />' in result


@pytest.mark.sphinx('html', testroot='toctree-glob',
                    confoverrides={'html_baseurl': 'https://example.com/subdir',
                                   'html_file_suffix': '.htm'})
def test_html_baseurl_and_html_file_suffix(app, status, warning):
    app.build()

    result = (app.outdir / 'index.htm').read_text(encoding='utf8')
    assert '<link rel="canonical" href="https://example.com/subdir/index.htm" />' in result

    result = (app.outdir / 'qux' / 'index.htm').read_text(encoding='utf8')
    assert '<link rel="canonical" href="https://example.com/subdir/qux/index.htm" />' in result


@pytest.mark.sphinx('html', testroot='basic')
def test_default_html_math_renderer(app, status, warning):
    assert app.builder.math_renderer_name == 'mathjax'


@pytest.mark.sphinx('html', testroot='basic',
                    confoverrides={'extensions': ['sphinx.ext.mathjax']})
def test_html_math_renderer_is_mathjax(app, status, warning):
    assert app.builder.math_renderer_name == 'mathjax'


@pytest.mark.sphinx('html', testroot='basic',
                    confoverrides={'extensions': ['sphinx.ext.imgmath']})
def test_html_math_renderer_is_imgmath(app, status, warning):
    assert app.builder.math_renderer_name == 'imgmath'


@pytest.mark.sphinx('html', testroot='basic',
                    confoverrides={'extensions': ['sphinxcontrib.jsmath',
                                                  'sphinx.ext.imgmath']})
def test_html_math_renderer_is_duplicated(make_app, app_params):
    args, kwargs = app_params
    with pytest.raises(
        ConfigError,
        match='Many math_renderers are registered. But no math_renderer is selected.',
    ):
        make_app(*args, **kwargs)


@pytest.mark.sphinx('html', testroot='basic',
                    confoverrides={'extensions': ['sphinx.ext.imgmath',
                                                  'sphinx.ext.mathjax']})
def test_html_math_renderer_is_duplicated2(app, status, warning):
    # case of both mathjax and another math_renderer is loaded
    assert app.builder.math_renderer_name == 'imgmath'  # The another one is chosen


@pytest.mark.sphinx('html', testroot='basic',
                    confoverrides={'extensions': ['sphinxcontrib.jsmath',
                                                  'sphinx.ext.imgmath'],
                                   'html_math_renderer': 'imgmath'})
def test_html_math_renderer_is_chosen(app, status, warning):
    assert app.builder.math_renderer_name == 'imgmath'


@pytest.mark.sphinx('html', testroot='basic',
                    confoverrides={'extensions': ['sphinxcontrib.jsmath',
                                                  'sphinx.ext.mathjax'],
                                   'html_math_renderer': 'imgmath'})
def test_html_math_renderer_is_mismatched(make_app, app_params):
    args, kwargs = app_params
    with pytest.raises(ConfigError, match="Unknown math_renderer 'imgmath' is given."):
        make_app(*args, **kwargs)


@pytest.mark.sphinx('html', testroot='basic')
def test_html_pygments_style_default(app):
    style = app.builder.highlighter.formatter_args.get('style')
    assert style.__name__ == 'Alabaster'


@pytest.mark.sphinx('html', testroot='basic',
                    confoverrides={'pygments_style': 'sphinx'})
def test_html_pygments_style_manually(app):
    style = app.builder.highlighter.formatter_args.get('style')
    assert style.__name__ == 'SphinxStyle'


@pytest.mark.sphinx('html', testroot='basic',
                    confoverrides={'html_theme': 'classic'})
def test_html_pygments_for_classic_theme(app):
    style = app.builder.highlighter.formatter_args.get('style')
    assert style.__name__ == 'SphinxStyle'


@pytest.mark.sphinx('html', testroot='basic')
def test_html_dark_pygments_style_default(app):
    assert app.builder.dark_highlighter is None


@pytest.mark.sphinx(testroot='basic', srcdir='validate_html_extra_path')
def test_validate_html_extra_path(app):
    (app.confdir / '_static').mkdir(parents=True, exist_ok=True)
    app.config.html_extra_path = [
        '/path/to/not_found',       # not found
        '_static',
        app.outdir,                 # outdir
        app.outdir / '_static',     # inside outdir
    ]
    validate_html_extra_path(app, app.config)
    assert app.config.html_extra_path == ['_static']


@pytest.mark.sphinx(testroot='basic', srcdir='validate_html_static_path')
def test_validate_html_static_path(app):
    (app.confdir / '_static').mkdir(parents=True, exist_ok=True)
    app.config.html_static_path = [
        '/path/to/not_found',       # not found
        '_static',
        app.outdir,                 # outdir
        app.outdir / '_static',     # inside outdir
    ]
    validate_html_static_path(app, app.config)
    assert app.config.html_static_path == ['_static']


@pytest.mark.sphinx(testroot='html_scaled_image_link')
def test_html_scaled_image_link(app):
    app.build()
    context = (app.outdir / 'index.html').read_text(encoding='utf8')

    # no scaled parameters
    assert re.search('\n<img alt="_images/img.png" src="_images/img.png" />', context)

    # scaled_image_link
    assert re.search('\n<a class="reference internal image-reference" href="_images/img.png">'
                     '<img alt="_images/img.png" src="_images/img.png" style="[^"]+" /></a>',
                     context)

    # no-scaled-link class disables the feature
    assert re.search('\n<img alt="_images/img.png" class="no-scaled-link"'
                     ' src="_images/img.png" style="[^"]+" />',
                     context)


@pytest.mark.sphinx('html', testroot='reST-code-block',
                    confoverrides={'html_codeblock_linenos_style': 'table'})
def test_html_codeblock_linenos_style_table(app):
    app.build()
    content = (app.outdir / 'index.html').read_text(encoding='utf8')

    assert ('<div class="linenodiv"><pre><span class="normal">1</span>\n'
            '<span class="normal">2</span>\n'
            '<span class="normal">3</span>\n'
            '<span class="normal">4</span></pre></div>') in content


@pytest.mark.sphinx('html', testroot='reST-code-block',
                    confoverrides={'html_codeblock_linenos_style': 'inline'})
def test_html_codeblock_linenos_style_inline(app):
    app.build()
    content = (app.outdir / 'index.html').read_text(encoding='utf8')

    assert '<span class="linenos">1</span>' in content


@pytest.mark.sphinx('html', testroot='highlight_options')
def test_highlight_options(app):
    subject = app.builder.highlighter
    with patch.object(subject, 'highlight_block', wraps=subject.highlight_block) as highlight:
        app.build()

        call_args = highlight.call_args_list
        assert len(call_args) == 3
        assert call_args[0] == call(ANY, 'default', force=False, linenos=False,
                                    location=ANY, opts={'default_option': True})
        assert call_args[1] == call(ANY, 'python', force=False, linenos=False,
                                    location=ANY, opts={'python_option': True})
        assert call_args[2] == call(ANY, 'java', force=False, linenos=False,
                                    location=ANY, opts={})


@pytest.mark.sphinx('html', testroot='highlight_options',
                    confoverrides={'highlight_options': {'default_option': True}})
def test_highlight_options_old(app):
    subject = app.builder.highlighter
    with patch.object(subject, 'highlight_block', wraps=subject.highlight_block) as highlight:
        app.build()

        call_args = highlight.call_args_list
        assert len(call_args) == 3
        assert call_args[0] == call(ANY, 'default', force=False, linenos=False,
                                    location=ANY, opts={'default_option': True})
        assert call_args[1] == call(ANY, 'python', force=False, linenos=False,
                                    location=ANY, opts={})
        assert call_args[2] == call(ANY, 'java', force=False, linenos=False,
                                    location=ANY, opts={})


@pytest.mark.sphinx('html', testroot='basic',
                    confoverrides={'html_permalinks': False})
def test_html_permalink_disable(app):
    app.build()
    content = (app.outdir / 'index.html').read_text(encoding='utf8')

    assert '<h1>The basic Sphinx documentation for testing</h1>' in content


@pytest.mark.sphinx('html', testroot='basic',
                    confoverrides={'html_permalinks_icon': '<span>[PERMALINK]</span>'})
def test_html_permalink_icon(app):
    app.build()
    content = (app.outdir / 'index.html').read_text(encoding='utf8')

    assert ('<h1>The basic Sphinx documentation for testing<a class="headerlink" '
            'href="#the-basic-sphinx-documentation-for-testing" '
            'title="Link to this heading"><span>[PERMALINK]</span></a></h1>' in content)


@pytest.mark.sphinx('html', testroot='html_signaturereturn_icon')
def test_html_signaturereturn_icon(app):
    app.build()
    content = (app.outdir / 'index.html').read_text(encoding='utf8')

    assert ('<span class="sig-return-icon">&#x2192;</span>' in content)


@pytest.mark.sphinx('html', testroot='reST-code-role')
def test_html_code_role(app):
    app.build()
    content = (app.outdir / 'index.html').read_text(encoding='utf8')

    common_content = (
        '<span class="k">def</span> <span class="nf">foo</span>'
        '<span class="p">(</span>'
        '<span class="mi">1</span> '
        '<span class="o">+</span> '
        '<span class="mi">2</span> '
        '<span class="o">+</span> '
        '<span class="kc">None</span> '
        '<span class="o">+</span> '
        '<span class="s2">&quot;abc&quot;</span>'
        '<span class="p">):</span> '
        '<span class="k">pass</span>')
    assert ('<p>Inline <code class="code highlight python docutils literal highlight-python">' +
            common_content + '</code> code block</p>') in content
    assert ('<div class="highlight-python notranslate">' +
            '<div class="highlight"><pre><span></span>' +
            common_content) in content


@pytest.mark.sphinx('html', testroot='root',
                    confoverrides={'option_emphasise_placeholders': True})
def test_option_emphasise_placeholders(app, status, warning):
    app.build()
    content = (app.outdir / 'objects.html').read_text(encoding='utf8')
    assert '<em><span class="pre">TYPE</span></em>' in content
    assert '{TYPE}' not in content
    assert ('<em><span class="pre">WHERE</span></em>'
            '<span class="pre">-</span>'
            '<em><span class="pre">COUNT</span></em>' in content)
    assert '<span class="pre">{{value}}</span>' in content
    assert ('<span class="pre">--plugin.option</span></span>'
            '<a class="headerlink" href="#cmdoption-perl-plugin.option" title="Link to this definition">¶</a></dt>') in content


@pytest.mark.sphinx('html', testroot='root')
def test_option_emphasise_placeholders_default(app, status, warning):
    app.build()
    content = (app.outdir / 'objects.html').read_text(encoding='utf8')
    assert '<span class="pre">={TYPE}</span>' in content
    assert '<span class="pre">={WHERE}-{COUNT}</span></span>' in content
    assert '<span class="pre">{client_name}</span>' in content
    assert ('<span class="pre">--plugin.option</span></span>'
            '<span class="sig-prename descclassname"></span>'
            '<a class="headerlink" href="#cmdoption-perl-plugin.option" title="Link to this definition">¶</a></dt>') in content


@pytest.mark.sphinx('html', testroot='root')
def test_option_reference_with_value(app, status, warning):
    app.build()
    content = (app.outdir / 'objects.html').read_text(encoding='utf-8')
    assert ('<span class="pre">-mapi</span></span><span class="sig-prename descclassname">'
            '</span><a class="headerlink" href="#cmdoption-git-commit-mapi"') in content
    assert 'first option <a class="reference internal" href="#cmdoption-git-commit-mapi">' in content
    assert ('<a class="reference internal" href="#cmdoption-git-commit-mapi">'
            '<code class="xref std std-option docutils literal notranslate"><span class="pre">-mapi[=xxx]</span></code></a>') in content
    assert '<span class="pre">-mapi</span> <span class="pre">with_space</span>' in content


@pytest.mark.sphinx('html', testroot='theming')
def test_theme_options(app, status, warning):
    app.build()

    result = (app.outdir / '_static' / 'documentation_options.js').read_text(encoding='utf8')
    assert 'NAVIGATION_WITH_KEYS: false' in result
    assert 'ENABLE_SEARCH_SHORTCUTS: true' in result


@pytest.mark.sphinx('html', testroot='theming',
                    confoverrides={'html_theme_options.navigation_with_keys': True,
                                   'html_theme_options.enable_search_shortcuts': False})
def test_theme_options_with_override(app, status, warning):
    app.build()

    result = (app.outdir / '_static' / 'documentation_options.js').read_text(encoding='utf8')
    assert 'NAVIGATION_WITH_KEYS: true' in result
    assert 'ENABLE_SEARCH_SHORTCUTS: false' in result


@pytest.mark.sphinx('html', testroot='build-html-theme-having-multiple-stylesheets')
def test_theme_having_multiple_stylesheets(app):
    app.build()
    content = (app.outdir / 'index.html').read_text(encoding='utf-8')

    assert '<link rel="stylesheet" type="text/css" href="_static/mytheme.css" />' in content
    assert '<link rel="stylesheet" type="text/css" href="_static/extra.css" />' in content


@pytest.mark.sphinx('html', testroot='images')
def test_copy_images(app, status, warning):
    app.build()

    images_dir = Path(app.outdir) / '_images'
    images = {image.name for image in images_dir.rglob('*')}
    assert images == {
        'img.png',
        'rimg.png',
        'rimg1.png',
        'svgimg.svg',
        'testimäge.png',
    }
