diff --git a/AUTHORS.rst b/AUTHORS.rst index 707c77aec04..098412b80f7 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -50,6 +50,7 @@ Contributors * Dimitri Papadopoulos Orfanos -- linting and spelling * Dmitry Shachnev -- modernisation and reproducibility * Doug Hellmann -- graphviz improvements +* Elijah Greenstein -- numfig_restart option * Eric Larson -- better error messages * Eric N. Vander Weele -- autodoc improvements * Eric Wieser -- autodoc improvements diff --git a/CHANGES.rst b/CHANGES.rst index 10ff0831699..618d32c03ca 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -68,6 +68,9 @@ Features added Patch by Jean-François B. * #13508: Initial support for :pep:`695` type aliases. Patch by Martin Matouš, Jeremy Maitin-Shepard, and Adam Turner. +* #14081: Add :confval:`numfig_restart` option to make it possible to restart + figure numbering at each ``:numbered:`` toctree. + Patch by Elijah Greenstein. * #14023: Add the new :confval:`mathjax_config_path` option to load MathJax configuration from a file. Patch by Randolf Scholz and Adam Turner. diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index e9f4d37c1de..5101e0cbbc4 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -359,6 +359,25 @@ Options for figure numbering .. versionadded:: 1.3 + +.. confval:: numfig_restart + :type: :code-py:`bool` + :default: :code-py:`False` + + If :code-py:`True`, then Sphinx restarts figure numbering every time Sphinx + encounters a :rst:dir:`toctree` directive with the ``:numbered:`` option + activated. You can use this option to make HTML/EPUB figure numbers + correspond to LaTeX figure numbers when using :confval:`latex_documents` to + group files into multiple LaTeX documents. + + .. note:: + + This option does not change the figure numbers assigned by the LaTeX + builder. + + .. versionadded:: 8.3 + + .. confval:: numfig_secnum_depth :type: :code-py:`int` :default: :code-py:`1` diff --git a/sphinx/config.py b/sphinx/config.py index f82e2b761ee..28bc6c35c94 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -288,6 +288,7 @@ class Config: 'numfig_secnum_depth': _Opt(1, 'env', frozenset((int, types.NoneType))), # numfig_format will be initialized in init_numfig_format() 'numfig_format': _Opt({}, 'env', frozenset((dict,))), + 'numfig_restart': _Opt(False, 'env', frozenset((bool,))), 'maximum_signature_line_length': _Opt( None, 'env', frozenset((int, types.NoneType)) ), diff --git a/sphinx/environment/collectors/toctree.py b/sphinx/environment/collectors/toctree.py index 5c3d5c97f8c..283dfeaa621 100644 --- a/sphinx/environment/collectors/toctree.py +++ b/sphinx/environment/collectors/toctree.py @@ -349,6 +349,8 @@ def _walk_doctree( else: _walk_doctree(docname, subnode, secnum) elif isinstance(subnode, addnodes.toctree): + if docname in env.numbered_toctrees and env.config.numfig_restart: + fignum_counter.clear() for _title, subdocname in subnode['entries']: if url_re.match(subdocname) or subdocname == 'self': # don't mess with those diff --git a/tests/test_builders/test_build_html_numfig.py b/tests/test_builders/test_build_html_numfig.py index 144d9958d0d..7e5171ffcd9 100644 --- a/tests/test_builders/test_build_html_numfig.py +++ b/tests/test_builders/test_build_html_numfig.py @@ -1143,3 +1143,437 @@ def test_numfig_with_singlehtml( ) -> None: app.build() check_xpath(cached_etree_parse(app.outdir / 'index.html'), 'index.html', *expect) + + +@pytest.mark.parametrize( + ('fname', 'path', 'check', 'be_found'), + [ + ( + 'index.html', + FIGURE_CAPTION + "/span[@class='caption-number']", + '^Fig. 1 $', + True, + ), + ( + 'index.html', + FIGURE_CAPTION + "/span[@class='caption-number']", + '^Fig. 2 $', + True, + ), + ( + 'index.html', + ".//table/caption/span[@class='caption-number']", + '^Table 1 $', + True, + ), + ( + 'index.html', + ".//table/caption/span[@class='caption-number']", + '^Table 2 $', + True, + ), + ( + 'index.html', + ".//div[@class='code-block-caption']/span[@class='caption-number']", + '^Listing 1 $', + True, + ), + ( + 'index.html', + ".//div[@class='code-block-caption']/span[@class='caption-number']", + '^Listing 2 $', + True, + ), + ('index.html', './/li/p/a/span', '^Fig. 1$', True), + ('index.html', './/li/p/a/span', '^Figure1.2$', True), # baz.rst + ('index.html', './/li/p/a/span', '^Table 1$', True), + ('index.html', './/li/p/a/span', '^Table:1.2$', True), # baz.rst + ('index.html', './/li/p/a/span', '^Listing 1$', True), + ('index.html', './/li/p/a/span', '^Code-1.2$', True), # baz.rst + ('index.html', './/li/p/a/span', '^Section.1$', True), + ('index.html', './/li/p/a/span', '^Section.1.1$', True), # bar.rst + ('index.html', './/li/p/a/span', '^Fig.1 should be Fig.1$', True), + ('index.html', './/li/p/a/span', '^Sect.1 Foo$', True), + ( + 'foo.html', + FIGURE_CAPTION + "/span[@class='caption-number']", + '^Fig. 1.1 $', + True, + ), + ( + 'foo.html', + FIGURE_CAPTION + "/span[@class='caption-number']", + '^Fig. 1.2 $', + True, + ), + ( + 'foo.html', + FIGURE_CAPTION + "/span[@class='caption-number']", + '^Fig. 1.3 $', + True, + ), + ( + 'foo.html', + FIGURE_CAPTION + "/span[@class='caption-number']", + '^Fig. 1.4 $', + True, + ), + ( + 'foo.html', + ".//table/caption/span[@class='caption-number']", + '^Table 1.1 $', + True, + ), + ( + 'foo.html', + ".//table/caption/span[@class='caption-number']", + '^Table 1.2 $', + True, + ), + ( + 'foo.html', + ".//table/caption/span[@class='caption-number']", + '^Table 1.3 $', + True, + ), + ( + 'foo.html', + ".//table/caption/span[@class='caption-number']", + '^Table 1.4 $', + True, + ), + ( + 'foo.html', + ".//div[@class='code-block-caption']/span[@class='caption-number']", + '^Listing 1.1 $', + True, + ), + ( + 'foo.html', + ".//div[@class='code-block-caption']/span[@class='caption-number']", + '^Listing 1.2 $', + True, + ), + ( + 'foo.html', + ".//div[@class='code-block-caption']/span[@class='caption-number']", + '^Listing 1.3 $', + True, + ), + ( + 'foo.html', + ".//div[@class='code-block-caption']/span[@class='caption-number']", + '^Listing 1.4 $', + True, + ), + ( + 'bar.html', + FIGURE_CAPTION + "/span[@class='caption-number']", + '^Fig. 1.1 $', + True, + ), + ( + 'bar.html', + FIGURE_CAPTION + "/span[@class='caption-number']", + '^Fig. 1.3 $', + True, + ), + ( + 'bar.html', + FIGURE_CAPTION + "/span[@class='caption-number']", + '^Fig. 1.4 $', + True, + ), + ( + 'bar.html', + ".//table/caption/span[@class='caption-number']", + '^Table 1.1 $', + True, + ), + ( + 'bar.html', + ".//table/caption/span[@class='caption-number']", + '^Table 1.3 $', + True, + ), + ( + 'bar.html', + ".//table/caption/span[@class='caption-number']", + '^Table 1.4 $', + True, + ), + ( + 'bar.html', + ".//div[@class='code-block-caption']/span[@class='caption-number']", + '^Listing 1.1 $', + True, + ), + ( + 'bar.html', + ".//div[@class='code-block-caption']/span[@class='caption-number']", + '^Listing 1.3 $', + True, + ), + ( + 'bar.html', + ".//div[@class='code-block-caption']/span[@class='caption-number']", + '^Listing 1.4 $', + True, + ), + ( + 'baz.html', + FIGURE_CAPTION + "/span[@class='caption-number']", + '^Fig. 1.2 $', + True, + ), + ( + 'baz.html', + ".//table/caption/span[@class='caption-number']", + '^Table 1.2 $', + True, + ), + ( + 'baz.html', + ".//div[@class='code-block-caption']/span[@class='caption-number']", + '^Listing 1.2 $', + True, + ), + ], +) +@pytest.mark.sphinx( + 'html', + testroot='numfig', + confoverrides={'numfig': True, 'numfig_restart': True}, +) +@pytest.mark.test_params(shared_result='test_build_html_numfig_restart') +def test_numfig_restart( + app: SphinxTestApp, + cached_etree_parse: Callable[[Path], ElementTree], + fname: str, + path: str, + check: str | None, + be_found: bool, +) -> None: + # Set up two numbered toctrees + index = (app.srcdir / 'index.rst').read_text(encoding='utf8') + index = index.replace(' foo', ' foo\n\n.. toctree::\n :numbered:\n') + (app.srcdir / 'index.rst').write_text(index, encoding='utf8') + app.build() + check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check, be_found) + + +@pytest.mark.parametrize( + ('fname', 'path', 'check', 'be_found'), + [ + ( + 'index.html', + FIGURE_CAPTION + "/span[@class='caption-number']", + '^Fig. 1 $', + True, + ), + ( + 'index.html', + FIGURE_CAPTION + "/span[@class='caption-number']", + '^Fig. 2 $', + True, + ), + ( + 'index.html', + ".//table/caption/span[@class='caption-number']", + '^Table 1 $', + True, + ), + ( + 'index.html', + ".//table/caption/span[@class='caption-number']", + '^Table 2 $', + True, + ), + ( + 'index.html', + ".//div[@class='code-block-caption']/span[@class='caption-number']", + '^Listing 1 $', + True, + ), + ( + 'index.html', + ".//div[@class='code-block-caption']/span[@class='caption-number']", + '^Listing 2 $', + True, + ), + ('index.html', './/li/p/a/span', '^Fig. 1$', True), + ('index.html', './/li/p/a/span', '^Figure1.1.2$', True), # baz.rst + ('index.html', './/li/p/a/span', '^Table 1$', True), + ('index.html', './/li/p/a/span', '^Table:1.1.2$', True), # baz.rst + ('index.html', './/li/p/a/span', '^Listing 1$', True), + ('index.html', './/li/p/a/span', '^Code-1.1.2$', True), # baz.rst + ('index.html', './/li/p/a/span', '^Section.1$', True), + ('index.html', './/li/p/a/span', '^Section.1.1$', True), # bar.rst + ('index.html', './/li/p/a/span', '^Fig.1 should be Fig.1$', True), + ('index.html', './/li/p/a/span', '^Sect.1 Foo$', True), + ( + 'foo.html', + FIGURE_CAPTION + "/span[@class='caption-number']", + '^Fig. 1.1 $', + True, + ), + ( + 'foo.html', + FIGURE_CAPTION + "/span[@class='caption-number']", + '^Fig. 1.1.1 $', + True, + ), + ( + 'foo.html', + FIGURE_CAPTION + "/span[@class='caption-number']", + '^Fig. 1.1.2 $', + True, + ), + ( + 'foo.html', + FIGURE_CAPTION + "/span[@class='caption-number']", + '^Fig. 1.2.1 $', + True, + ), + ( + 'foo.html', + ".//table/caption/span[@class='caption-number']", + '^Table 1.1 $', + True, + ), + ( + 'foo.html', + ".//table/caption/span[@class='caption-number']", + '^Table 1.1.1 $', + True, + ), + ( + 'foo.html', + ".//table/caption/span[@class='caption-number']", + '^Table 1.1.2 $', + True, + ), + ( + 'foo.html', + ".//table/caption/span[@class='caption-number']", + '^Table 1.2.1 $', + True, + ), + ( + 'foo.html', + ".//div[@class='code-block-caption']/span[@class='caption-number']", + '^Listing 1.1 $', + True, + ), + ( + 'foo.html', + ".//div[@class='code-block-caption']/span[@class='caption-number']", + '^Listing 1.1.1 $', + True, + ), + ( + 'foo.html', + ".//div[@class='code-block-caption']/span[@class='caption-number']", + '^Listing 1.1.2 $', + True, + ), + ( + 'foo.html', + ".//div[@class='code-block-caption']/span[@class='caption-number']", + '^Listing 1.2.1 $', + True, + ), + ( + 'bar.html', + FIGURE_CAPTION + "/span[@class='caption-number']", + '^Fig. 1.1.1 $', + True, + ), + ( + 'bar.html', + FIGURE_CAPTION + "/span[@class='caption-number']", + '^Fig. 1.1.3 $', + True, + ), + ( + 'bar.html', + FIGURE_CAPTION + "/span[@class='caption-number']", + '^Fig. 1.2.1 $', + True, + ), + ( + 'bar.html', + ".//table/caption/span[@class='caption-number']", + '^Table 1.1.1 $', + True, + ), + ( + 'bar.html', + ".//table/caption/span[@class='caption-number']", + '^Table 1.1.3 $', + True, + ), + ( + 'bar.html', + ".//table/caption/span[@class='caption-number']", + '^Table 1.2.1 $', + True, + ), + ( + 'bar.html', + ".//div[@class='code-block-caption']/span[@class='caption-number']", + '^Listing 1.1.1 $', + True, + ), + ( + 'bar.html', + ".//div[@class='code-block-caption']/span[@class='caption-number']", + '^Listing 1.1.3 $', + True, + ), + ( + 'bar.html', + ".//div[@class='code-block-caption']/span[@class='caption-number']", + '^Listing 1.2.1 $', + True, + ), + ( + 'baz.html', + FIGURE_CAPTION + "/span[@class='caption-number']", + '^Fig. 1.1.2 $', + True, + ), + ( + 'baz.html', + ".//table/caption/span[@class='caption-number']", + '^Table 1.1.2 $', + True, + ), + ( + 'baz.html', + ".//div[@class='code-block-caption']/span[@class='caption-number']", + '^Listing 1.1.2 $', + True, + ), + ], +) +@pytest.mark.sphinx( + 'html', + testroot='numfig', + confoverrides={'numfig': True, 'numfig_restart': True, 'numfig_secnum_depth': 2}, +) +@pytest.mark.test_params(shared_result='test_build_html_numfig_restart_secnum_depth_2') +def test_numfig_restart_secnum_depth_2( + app: SphinxTestApp, + cached_etree_parse: Callable[[Path], ElementTree], + fname: str, + path: str, + check: str | None, + be_found: bool, +) -> None: + # Set up two numbered toctrees + index = (app.srcdir / 'index.rst').read_text(encoding='utf8') + index = index.replace(' foo', ' foo\n\n.. toctree::\n :numbered:\n') + (app.srcdir / 'index.rst').write_text(index, encoding='utf8') + app.build() + check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check, be_found)