diff --git a/.gitignore b/.gitignore index 5d50c0a..e825830 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ build/ dist/ output/ -sphinxcontrib/__pycache__ +__pycache__ sphinxcontrib_restbuilder.egg-info .tox/ diff --git a/sphinxcontrib/builders/singlerst.py b/sphinxcontrib/builders/singlerst.py new file mode 100644 index 0000000..49c6b89 --- /dev/null +++ b/sphinxcontrib/builders/singlerst.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +""" + sphinxcontrib.builders.singlerst + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Single ReST file Sphinx builder. + + :copyright: Copyright 2012-2021 by Freek Dijkstra and contributors. + :license: BSD, see LICENSE.txt for details. +""" + +from __future__ import (print_function, unicode_literals, absolute_import) + +from sphinx.util.nodes import inline_all_toctrees + +from .rst import RstBuilder +from ..writers.rst import RstWriter + + +class SingleRstBuilder(RstBuilder): + """ + A builder that combines all documents into a single RST file. + """ + name = 'singlerst' + + def get_outdated_docs(self): + """ + Return an iterable of input files that are outdated. + """ + # Since all documents are combined into one, we always rebuild. + return 'all documents' + + def get_target_uri(self, docname, typ=None): + if docname == self.config.root_doc: + return '' + return '#document-' + docname + + def assemble_doctree(self): + """Assemble all documents into a single doctree.""" + root_doc = self.config.root_doc + tree = self.env.get_doctree(root_doc) + # Inline all toctrees to merge all documents into the root doctree. + tree = inline_all_toctrees( + self, set(), root_doc, tree, colorfunc=lambda x: x, traversed=[] + ) + tree['docname'] = root_doc + # Resolve all references now that all documents are combined. + self.env.resolve_references(tree, root_doc, self) + return tree + + def prepare_writing(self, docnames): + self.writer = RstWriter(self) + + def write(self, build_docnames, updated_docnames, method='update'): + # This method overrides the base builder's write() to combine + # all documents into a single file instead of writing each separately. + docnames = self.env.all_docs + self.prepare_writing(docnames) + doctree = self.assemble_doctree() + self.write_doc(self.config.root_doc, doctree) + + def finish(self): + pass diff --git a/sphinxcontrib/restbuilder.py b/sphinxcontrib/restbuilder.py index 7998395..75dfa54 100644 --- a/sphinxcontrib/restbuilder.py +++ b/sphinxcontrib/restbuilder.py @@ -21,9 +21,11 @@ def setup(app): # even if Sphinx is not yet installed. from sphinx.writers.text import STDINDENT from .builders.rst import RstBuilder # loads RstWriter as well. + from .builders.singlerst import SingleRstBuilder app.require_sphinx('1.4') app.add_builder(RstBuilder) + app.add_builder(SingleRstBuilder) app.add_config_value('rst_file_suffix', ".rst", False) """This is the file name suffix for reST files""" app.add_config_value('rst_link_suffix', None, False) diff --git a/sphinxcontrib/writers/rst.py b/sphinxcontrib/writers/rst.py index 91b53b6..dc74306 100644 --- a/sphinxcontrib/writers/rst.py +++ b/sphinxcontrib/writers/rst.py @@ -857,6 +857,15 @@ def visit_raw(self, node): def visit_docinfo(self, node): raise nodes.SkipNode + def visit_start_of_file(self, node): + # Emit anchor target for document boundary (used by singlerst builder) + self.new_state(0) + self.add_text('.. _document-%s:' % node['docname']) + self.end_state(wrap=False) + + def depart_start_of_file(self, node): + pass + def unknown_visit(self, node): self.log_unknown(node.__class__.__name__, node) diff --git a/tests/test_singlerst.py b/tests/test_singlerst.py new file mode 100644 index 0000000..2c5b32b --- /dev/null +++ b/tests/test_singlerst.py @@ -0,0 +1,45 @@ +from os.path import join, exists +import io + +from tests.utils import build_singlerst + + +def test_singlerst_combines_documents(src_dir, output_dir): + """Test that singlerst builder combines multiple documents into one file.""" + test_src = join(src_dir, 'sphinx-directives/toctree') + test_output = join(output_dir, 'singlerst/toctree') + + build_singlerst(test_src, test_output) + + # Should produce a single index.rst file + output_file = join(test_output, 'index.rst') + assert exists(output_file), "singlerst should create index.rst" + + # Read the output and verify it contains content from all documents + with io.open(output_file, encoding='utf-8') as f: + content = f.read() + + # Check that content from doc1 and doc2 is included + assert 'Doc 1' in content, "Output should contain Doc 1 heading" + assert 'This is document 1' in content, "Output should contain doc1 content" + assert 'Doc 2' in content, "Output should contain Doc 2 heading" + assert 'This is document 2' in content, "Output should contain doc2 content" + + # Check that document boundary anchors are generated + assert '.. _document-doc1:' in content, "Output should have doc1 anchor" + assert '.. _document-doc2:' in content, "Output should have doc2 anchor" + + +def test_singlerst_single_file_only(src_dir, output_dir): + """Test that singlerst builder produces only one RST file.""" + test_src = join(src_dir, 'sphinx-directives/toctree') + test_output = join(output_dir, 'singlerst/toctree-single') + + build_singlerst(test_src, test_output) + + # Should NOT produce separate doc1.rst and doc2.rst files + assert not exists(join(test_output, 'doc1.rst')), "singlerst should not create doc1.rst" + assert not exists(join(test_output, 'doc2.rst')), "singlerst should not create doc2.rst" + + # Should only have index.rst + assert exists(join(test_output, 'index.rst')), "singlerst should create index.rst" diff --git a/tests/utils.py b/tests/utils.py index 6070b02..1fdf159 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -35,6 +35,30 @@ def docutils_namespace(): roles._roles = _roles +def build_singlerst(src_dir, output_dir, config={}): + """Build using the singlerst builder instead of rst.""" + doctrees_dir = join(output_dir, '.doctrees') + + default_config = { + 'extensions': ['sphinxcontrib.restbuilder'], + 'master_doc': 'index', + } + default_config.update(config) + config = default_config + + with docutils_namespace(): + app = Sphinx( + src_dir, + None, + output_dir, + doctrees_dir, + 'singlerst', + confoverrides=config, + verbosity=0, + ) + app.build(force_all=True) + + def build_sphinx(src_dir, output_dir, files=None, config={}): doctrees_dir = join(output_dir, '.doctrees')