"""Test the HTML builder and check output against XPath.""" from __future__ import annotations import os import posixpath import re from typing import TYPE_CHECKING import pytest from sphinx._cli.util.errors import strip_escape_sequences from sphinx.builders.html import validate_html_extra_path, validate_html_static_path from sphinx.errors import ConfigError from sphinx.util.inventory import InventoryFile, _InventoryItem from tests.test_builders.xpath_data import FIGURE_CAPTION from tests.test_builders.xpath_util import check_xpath if TYPE_CHECKING: from typing import Any def test_html_sidebars_error(make_app, tmp_path): (tmp_path / 'conf.py').touch() with pytest.raises( ConfigError, match="Values in 'html_sidebars' must be a list of strings. " "At least one pattern has a string value: 'index'. " r"Change to `html_sidebars = \{'index': \['searchbox.html'\]\}`.", ): make_app( 'html', srcdir=tmp_path, confoverrides={'html_sidebars': {'index': 'searchbox.html'}}, ) def test_html4_error(make_app, tmp_path): (tmp_path / 'conf.py').touch() with pytest.raises( ConfigError, match='HTML 4 is no longer supported by Sphinx', ): make_app( 'html', srcdir=tmp_path, confoverrides={'html4_writer': True}, ) @pytest.mark.parametrize( ('fname', 'path', 'check'), [ ('index.html', ".//div[@class='citation']/span", r'Ref1'), ('index.html', ".//div[@class='citation']/span", r'Ref_1'), ( 'footnote.html', ".//a[@class='footnote-reference brackets'][@href='#id9'][@id='id1']", r'1', ), ( 'footnote.html', ".//a[@class='footnote-reference brackets'][@href='#id10'][@id='id2']", r'2', ), ( 'footnote.html', ".//a[@class='footnote-reference brackets'][@href='#foo'][@id='id3']", r'3', ), ( 'footnote.html', ".//a[@class='reference internal'][@href='#bar'][@id='id4']/span", r'\[bar\]', ), ( 'footnote.html', ".//a[@class='reference internal'][@href='#baz-qux'][@id='id5']/span", r'\[baz_qux\]', ), ( 'footnote.html', ".//a[@class='footnote-reference brackets'][@href='#id11'][@id='id6']", r'4', ), ( 'footnote.html', ".//a[@class='footnote-reference brackets'][@href='#id12'][@id='id7']", r'5', ), ( 'footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id1']", r'1', ), ( 'footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id2']", r'2', ), ( 'footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id3']", r'3', ), ('footnote.html', ".//div[@class='citation']/span/a[@href='#id4']", r'bar'), ('footnote.html', ".//div[@class='citation']/span/a[@href='#id5']", r'baz_qux'), ( 'footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id6']", r'4', ), ( 'footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id7']", r'5', ), ( 'footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id8']", r'6', ), ], ) @pytest.mark.sphinx('html', testroot='root') @pytest.mark.test_params(shared_result='test_build_html_output_docutils18') def test_docutils_output(app, cached_etree_parse, fname, path, check): app.build() check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check) @pytest.mark.sphinx( 'html', testroot='root', parallel=2, ) def test_html_parallel(app): app.build() @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( 'expect', [ (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, expect): app.build() check_xpath(cached_etree_parse(app.outdir / 'index.html'), 'index.html', *expect) @pytest.mark.sphinx( 'html', testroot='basic', confoverrides={'html_copy_source': False}, ) def test_html_copy_source(app): app.build(force_all=True) 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.build(force_all=True) 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.build(force_all=True) 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.build(force_all=True) assert (app.outdir / '_sources' / 'index.rst').exists() @pytest.mark.sphinx('html', testroot='html_entity') def test_html_entity(app): app.build(force_all=True) 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.MULTILINE): assert entity not in valid_entities @pytest.mark.sphinx('html', testroot='basic') def test_html_inventory(app): app.build(force_all=True) 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'] == _InventoryItem( project_name='Project name not set', project_version='', uri='https://www.google.com/py-modindex.html', display_name='Module Index', ) assert invdata['std:label']['py-modindex'] == _InventoryItem( project_name='Project name not set', project_version='', uri='https://www.google.com/py-modindex.html', display_name='Python Module Index', ) assert invdata['std:label']['genindex'] == _InventoryItem( project_name='Project name not set', project_version='', uri='https://www.google.com/genindex.html', display_name='Index', ) assert invdata['std:label']['search'] == _InventoryItem( project_name='Project name not set', project_version='', uri='https://www.google.com/search.html', display_name='Search Page', ) assert set(invdata['std:doc'].keys()) == {'index'} assert invdata['std:doc']['index'] == _InventoryItem( project_name='Project name not set', project_version='', uri='https://www.google.com/index.html', display_name='The basic Sphinx documentation for testing', ) @pytest.mark.usefixtures('_http_teapot') @pytest.mark.sphinx( 'html', testroot='images', confoverrides={'html_sourcelink_suffix': ''}, ) def test_html_anchor_for_figure(app): app.build(force_all=True) content = (app.outdir / 'index.html').read_text(encoding='utf8') assert ( '
\n

The caption of pic' '

\n
' ) in content @pytest.mark.sphinx('html', testroot='directives-raw') def test_html_raw_directive(app): app.build(force_all=True) 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 '

HTML: abc def ghi

' in result assert '

LaTeX: abc ghi

' in result @pytest.mark.parametrize( 'expect', [ (".//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, expect): app.build() check_xpath(cached_etree_parse(app.outdir / 'index.html'), 'index.html', *expect) @pytest.mark.sphinx('html', testroot='html_style') def test_html_style(app): app.build() result = (app.outdir / 'index.html').read_text(encoding='utf8') assert ( '' ) in result assert ( '' ) not in result @pytest.mark.sphinx( 'html', testroot='basic', confoverrides={ 'html_sidebars': { '**': [ 'localtoc.html', 'searchfield.html', 'sourcelink.html', ] } }, ) def test_html_sidebar(app): ctx: dict[str, Any] = {} # default for alabaster app.build(force_all=True) result = (app.outdir / 'index.html').read_text(encoding='utf8') # layout.html assert '