Skip to content

Commit 37317cc

Browse files
committed
Add singlerst builder
1 parent c8b9c0d commit 37317cc

6 files changed

Lines changed: 144 additions & 1 deletion

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
build/
22
dist/
33
output/
4-
sphinxcontrib/__pycache__
4+
__pycache__
55
sphinxcontrib_restbuilder.egg-info
66
.tox/
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
sphinxcontrib.builders.singlerst
4+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5+
6+
Single ReST file Sphinx builder.
7+
8+
:copyright: Copyright 2012-2021 by Freek Dijkstra and contributors.
9+
:license: BSD, see LICENSE.txt for details.
10+
"""
11+
12+
from __future__ import (print_function, unicode_literals, absolute_import)
13+
14+
from sphinx.util.nodes import inline_all_toctrees
15+
16+
from .rst import RstBuilder
17+
from ..writers.rst import RstWriter
18+
19+
20+
class SingleRstBuilder(RstBuilder):
21+
"""
22+
A builder that combines all documents into a single RST file.
23+
"""
24+
name = 'singlerst'
25+
26+
def get_outdated_docs(self):
27+
"""
28+
Return an iterable of input files that are outdated.
29+
"""
30+
# Since all documents are combined into one, we always rebuild.
31+
return 'all documents'
32+
33+
def get_target_uri(self, docname, typ=None):
34+
if docname == self.config.root_doc:
35+
return ''
36+
return '#document-' + docname
37+
38+
def assemble_doctree(self):
39+
"""Assemble all documents into a single doctree."""
40+
root_doc = self.config.root_doc
41+
tree = self.env.get_doctree(root_doc)
42+
# Inline all toctrees to merge all documents into the root doctree.
43+
tree = inline_all_toctrees(
44+
self, set(), root_doc, tree, colorfunc=lambda x: x, traversed=[]
45+
)
46+
tree['docname'] = root_doc
47+
# Resolve all references now that all documents are combined.
48+
self.env.resolve_references(tree, root_doc, self)
49+
return tree
50+
51+
def prepare_writing(self, docnames):
52+
self.writer = RstWriter(self)
53+
54+
def write(self, build_docnames, updated_docnames, method='update'):
55+
# This method overrides the base builder's write() to combine
56+
# all documents into a single file instead of writing each separately.
57+
docnames = self.env.all_docs
58+
self.prepare_writing(docnames)
59+
doctree = self.assemble_doctree()
60+
self.write_doc(self.config.root_doc, doctree)
61+
62+
def finish(self):
63+
pass

sphinxcontrib/restbuilder.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ def setup(app):
2121
# even if Sphinx is not yet installed.
2222
from sphinx.writers.text import STDINDENT
2323
from .builders.rst import RstBuilder # loads RstWriter as well.
24+
from .builders.singlerst import SingleRstBuilder
2425

2526
app.require_sphinx('1.4')
2627
app.add_builder(RstBuilder)
28+
app.add_builder(SingleRstBuilder)
2729
app.add_config_value('rst_file_suffix', ".rst", False)
2830
"""This is the file name suffix for reST files"""
2931
app.add_config_value('rst_link_suffix', None, False)

sphinxcontrib/writers/rst.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,15 @@ def visit_raw(self, node):
857857
def visit_docinfo(self, node):
858858
raise nodes.SkipNode
859859

860+
def visit_start_of_file(self, node):
861+
# Emit anchor target for document boundary (used by singlerst builder)
862+
self.new_state(0)
863+
self.add_text('.. _document-%s:' % node['docname'])
864+
self.end_state(wrap=False)
865+
866+
def depart_start_of_file(self, node):
867+
pass
868+
860869
def unknown_visit(self, node):
861870
self.log_unknown(node.__class__.__name__, node)
862871

tests/test_singlerst.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from os.path import join, exists
2+
import io
3+
4+
from tests.utils import build_singlerst
5+
6+
7+
def test_singlerst_combines_documents(src_dir, output_dir):
8+
"""Test that singlerst builder combines multiple documents into one file."""
9+
test_src = join(src_dir, 'sphinx-directives/toctree')
10+
test_output = join(output_dir, 'singlerst/toctree')
11+
12+
build_singlerst(test_src, test_output)
13+
14+
# Should produce a single index.rst file
15+
output_file = join(test_output, 'index.rst')
16+
assert exists(output_file), "singlerst should create index.rst"
17+
18+
# Read the output and verify it contains content from all documents
19+
with io.open(output_file, encoding='utf-8') as f:
20+
content = f.read()
21+
22+
# Check that content from doc1 and doc2 is included
23+
assert 'Doc 1' in content, "Output should contain Doc 1 heading"
24+
assert 'This is document 1' in content, "Output should contain doc1 content"
25+
assert 'Doc 2' in content, "Output should contain Doc 2 heading"
26+
assert 'This is document 2' in content, "Output should contain doc2 content"
27+
28+
# Check that document boundary anchors are generated
29+
assert '.. _document-doc1:' in content, "Output should have doc1 anchor"
30+
assert '.. _document-doc2:' in content, "Output should have doc2 anchor"
31+
32+
33+
def test_singlerst_single_file_only(src_dir, output_dir):
34+
"""Test that singlerst builder produces only one RST file."""
35+
test_src = join(src_dir, 'sphinx-directives/toctree')
36+
test_output = join(output_dir, 'singlerst/toctree-single')
37+
38+
build_singlerst(test_src, test_output)
39+
40+
# Should NOT produce separate doc1.rst and doc2.rst files
41+
assert not exists(join(test_output, 'doc1.rst')), "singlerst should not create doc1.rst"
42+
assert not exists(join(test_output, 'doc2.rst')), "singlerst should not create doc2.rst"
43+
44+
# Should only have index.rst
45+
assert exists(join(test_output, 'index.rst')), "singlerst should create index.rst"

tests/utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,30 @@ def docutils_namespace():
3535
roles._roles = _roles
3636

3737

38+
def build_singlerst(src_dir, output_dir, config={}):
39+
"""Build using the singlerst builder instead of rst."""
40+
doctrees_dir = join(output_dir, '.doctrees')
41+
42+
default_config = {
43+
'extensions': ['sphinxcontrib.restbuilder'],
44+
'master_doc': 'index',
45+
}
46+
default_config.update(config)
47+
config = default_config
48+
49+
with docutils_namespace():
50+
app = Sphinx(
51+
src_dir,
52+
None,
53+
output_dir,
54+
doctrees_dir,
55+
'singlerst',
56+
confoverrides=config,
57+
verbosity=0,
58+
)
59+
app.build(force_all=True)
60+
61+
3862
def build_sphinx(src_dir, output_dir, files=None, config={}):
3963
doctrees_dir = join(output_dir, '.doctrees')
4064

0 commit comments

Comments
 (0)