From 56b212d44dc5e82b9c80ddbcd67783bc18146c97 Mon Sep 17 00:00:00 2001 From: christian polzer Date: Tue, 18 Aug 2026 16:05:49 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20NEW:=20Render=20marked-RST=20bl?= =?UTF-8?q?ocks=20in=20src-trace=20directive=20(#43)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `src-trace` directive previously only rendered one-line needs and silently discarded marked-RST blocks even when `get_rst = true` was set. Marked-RST support is now opt-in via the existing `get_rst` toggle: when enabled, each extracted RST block is parsed inline into the current document with `nested_parse_with_titles`, giving authors full control over the emitted nodes (needs, cross-references, admonitions, ...). Source-page anchor mappings are updated for marked-RST blocks too, so the generated highlighted source page links the marker line back to the document that hosts the `src-trace` directive. - Extend `render_needs()` in `sphinx_extension/directives/src_trace.py` with a `_render_marked_rst()` helper. - Drop the "only supports one-line needs" attention banner in `docs/source/components/directive.rst` and document the opt-in. - Add a `doc_test/marked_rst_basic` Sphinx fixture and doctree snapshot covering `.. impl::` rendered from a C++ block comment. Refs useblocks/sphinx-codelinks#43 --- docs/source/components/directive.rst | 36 +++++++++- .../sphinx_extension/directives/src_trace.py | 72 ++++++++++++++++++- ...[sphinx_project6-source_code6].doctree.xml | 5 ++ tests/doc_test/marked_rst_basic/conf.py | 12 ++++ tests/doc_test/marked_rst_basic/dummy_src.cpp | 16 +++++ tests/doc_test/marked_rst_basic/index.rst | 2 + .../doc_test/marked_rst_basic/src_trace.toml | 2 + tests/test_src_trace.py | 4 ++ 8 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 tests/__snapshots__/test_src_trace/test_build_html[sphinx_project6-source_code6].doctree.xml create mode 100644 tests/doc_test/marked_rst_basic/conf.py create mode 100644 tests/doc_test/marked_rst_basic/dummy_src.cpp create mode 100644 tests/doc_test/marked_rst_basic/index.rst create mode 100644 tests/doc_test/marked_rst_basic/src_trace.toml diff --git a/docs/source/components/directive.rst b/docs/source/components/directive.rst index f885bee5..173ff892 100644 --- a/docs/source/components/directive.rst +++ b/docs/source/components/directive.rst @@ -3,8 +3,6 @@ Directive ========= -.. attention:: ``src-trace`` directive currently only supports :ref:`one-line need definition `. - ``CodeLinks`` provides ``src-trace`` directive and it can be used in the following ways: .. code-block:: rst @@ -76,3 +74,37 @@ The needs defined in source code are extracted and rendered to: :directory: ./discharge To have a more customized configuration of ``CodeLinks``, please refer to :ref:`configuration `. + +Marked reStructuredText +----------------------- + +In addition to :ref:`one-line needs `, the ``src-trace`` directive can +render :ref:`marked reStructuredText ` blocks extracted from source +code comments. Marked-RST support is opt-in and requires enabling +``get_rst = true`` for the project in your ``src_trace.toml`` (or via +``src_trace_projects`` in ``conf.py``). + +.. code-block:: toml + :caption: src_trace.toml + + [codelinks.projects.dcdc.analyse] + get_rst = true + +Each marked block is parsed inline into the current document, so the author has +full control over what is emitted — including custom directives such as +``.. impl::`` from sphinx-needs, cross-references, admonitions, or plain +paragraphs. Example marker in C++: + +.. code-block:: cpp + + /* + @rst + .. impl:: implement dummy function 1 + :id: IMPL_71 + @endrst + */ + void dummy_func1() {} + +When source page generation is enabled (``set_local_url = true``), the source +file line containing the marker is linked back to the document that hosts the +``src-trace`` directive. diff --git a/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py b/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py index 2db43165..a637a16e 100644 --- a/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py +++ b/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py @@ -6,13 +6,15 @@ from docutils import nodes from docutils.parsers.rst import directives +from docutils.statemachine import StringList from sphinx.util import logging from sphinx.util.docutils import SphinxDirective +from sphinx.util.nodes import nested_parse_with_titles from sphinx_needs.api import add_need # type: ignore[import-untyped] from sphinx_needs.utils import add_doc # type: ignore[import-untyped] from sphinx_codelinks.analyse.analyse import SourceAnalyse -from sphinx_codelinks.analyse.models import OneLineNeed +from sphinx_codelinks.analyse.models import MarkedRst, OneLineNeed from sphinx_codelinks.config import ( CodeLinksConfig, CodeLinksProjectConfigType, @@ -340,4 +342,72 @@ def render_needs( oneline_need.source_map["start"]["row"] + 1 ] = f"{docs_href}#{oneline_need.need['id']}" + for marked_rst in src_analyse.marked_rst: + rendered_needs.extend( + self._render_marked_rst(marked_rst, src_analyse, local_url_field, dirs) + ) + return rendered_needs + + def _render_marked_rst( + self, + marked_rst: MarkedRst, + src_analyse: SourceAnalyse, + local_url_field: str | None, + dirs: dict[str, Path], + ) -> list[nodes.Node]: + """Parse a marked-RST block inline into the doctree. + + The RST content extracted from the source comment is parsed with + ``nested_parse_with_titles`` so the author has full control over what + nodes are produced (needs, cross-references, admonitions, ...). The + block is also registered in :data:`file_lineno_href.mappings` so the + generated source-code page links the marker line back to the current + document. + + :param marked_rst: The extracted marked-RST block. + :param src_analyse: The active source analysis instance. + :param local_url_field: Configured local URL field name, or ``None``. + :param dirs: Directory mapping used by :meth:`render_needs`. + :return: The docutils nodes produced by parsing the RST block. + """ + filepath = src_analyse.analyse_config.src_dir / marked_rst.filepath + target_filepath = dirs["target_dir"] / filepath.relative_to(dirs["src_dir"]) + + if local_url_field: + # Copy the source file to the build tree so the generated source + # page (see ``generate_code_page`` on ``html-collect-pages``) can + # render it. Mirrors the one-line-need branch above. + target_filepath.parent.mkdir(parents=True, exist_ok=True) + target_filepath.write_text(filepath.read_text()) + + container = nodes.container() + container["classes"].append("src-trace-marked-rst") + + # ``StringList`` requires a per-line source anchor so warnings emitted + # by nested_parse point back to the original source file/line. + source_ref = str(filepath) + start_row = marked_rst.source_map["start"]["row"] + rst_lines = marked_rst.rst.splitlines() + string_list = StringList( + rst_lines, + items=[ + (source_ref, start_row + offset) for offset in range(len(rst_lines)) + ], + ) + + nested_parse_with_titles(self.state, string_list, container) + + if local_url_field: + # Point the source page anchor at the current document so users can + # navigate from the highlighted source line back to the rendered + # RST. Marked-RST blocks are not guaranteed to define a need id, so + # we deliberately link to the containing doc only. + _, docs_href = get_rel_path( + Path(self.env.docname), target_filepath, dirs["out_dir"] + ) + file_lineno_href.mappings.setdefault(str(target_filepath), {})[ + start_row + 1 + ] = str(docs_href) + + return list(container.children) diff --git a/tests/__snapshots__/test_src_trace/test_build_html[sphinx_project6-source_code6].doctree.xml b/tests/__snapshots__/test_src_trace/test_build_html[sphinx_project6-source_code6].doctree.xml new file mode 100644 index 00000000..cca41e93 --- /dev/null +++ b/tests/__snapshots__/test_src_trace/test_build_html[sphinx_project6-source_code6].doctree.xml @@ -0,0 +1,5 @@ + + + + + Body paragraph inside the marked RST block. diff --git a/tests/doc_test/marked_rst_basic/conf.py b/tests/doc_test/marked_rst_basic/conf.py new file mode 100644 index 00000000..04451b15 --- /dev/null +++ b/tests/doc_test/marked_rst_basic/conf.py @@ -0,0 +1,12 @@ +# Configuration file for the Sphinx documentation builder. +project = "marked-rst-demo" +copyright = "2026, useblocks" +author = "useblocks" + +extensions = ["sphinx_needs", "sphinx_codelinks"] + +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +src_trace_config_from_toml = "src_trace.toml" + +html_theme = "alabaster" diff --git a/tests/doc_test/marked_rst_basic/dummy_src.cpp b/tests/doc_test/marked_rst_basic/dummy_src.cpp new file mode 100644 index 00000000..0ab0b719 --- /dev/null +++ b/tests/doc_test/marked_rst_basic/dummy_src.cpp @@ -0,0 +1,16 @@ +#include + +/* +@rst +.. impl:: implement dummy function 1 + :id: IMPL_MRST_BASIC_1 + + Body paragraph inside the marked RST block. +@endrst +*/ +void dummy_func1() {} + +int main() { + dummy_func1(); + return 0; +} diff --git a/tests/doc_test/marked_rst_basic/index.rst b/tests/doc_test/marked_rst_basic/index.rst new file mode 100644 index 00000000..1750ff79 --- /dev/null +++ b/tests/doc_test/marked_rst_basic/index.rst @@ -0,0 +1,2 @@ +.. src-trace:: + :project: src diff --git a/tests/doc_test/marked_rst_basic/src_trace.toml b/tests/doc_test/marked_rst_basic/src_trace.toml new file mode 100644 index 00000000..e407d8f7 --- /dev/null +++ b/tests/doc_test/marked_rst_basic/src_trace.toml @@ -0,0 +1,2 @@ +[codelinks.projects.src.analyse] +get_rst = true diff --git a/tests/test_src_trace.py b/tests/test_src_trace.py index 7389e811..29f70c58 100644 --- a/tests/test_src_trace.py +++ b/tests/test_src_trace.py @@ -201,6 +201,10 @@ def test_src_tracing_config_positive(make_app: Callable[..., SphinxTestApp], tmp Path("doc_test") / "go_basic", Path("doc_test") / "go_basic", ), + ( + Path("doc_test") / "marked_rst_basic", + Path("doc_test") / "marked_rst_basic", + ), ], ) def test_build_html( From 0720313be151c48ccf96ae15ad2510f3def78dbd Mon Sep 17 00:00:00 2001 From: christian polzer Date: Wed, 19 Aug 2026 18:09:09 +0200 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=90=9B=20FIX:=20Add=20missing=20marke?= =?UTF-8?q?d=5Frst=20label=20anchor=20in=20directive.rst?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/components/directive.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/components/directive.rst b/docs/source/components/directive.rst index 173ff892..0d44411f 100644 --- a/docs/source/components/directive.rst +++ b/docs/source/components/directive.rst @@ -75,11 +75,13 @@ The needs defined in source code are extracted and rendered to: To have a more customized configuration of ``CodeLinks``, please refer to :ref:`configuration `. +.. _marked_rst: + Marked reStructuredText ----------------------- In addition to :ref:`one-line needs `, the ``src-trace`` directive can -render :ref:`marked reStructuredText ` blocks extracted from source +render marked reStructuredText blocks extracted from source code comments. Marked-RST support is opt-in and requires enabling ``get_rst = true`` for the project in your ``src_trace.toml`` (or via ``src_trace_projects`` in ``conf.py``). From a2c483943d34cf6e54f05654e8f65efc8907f927 Mon Sep 17 00:00:00 2001 From: christian polzer Date: Wed, 19 Aug 2026 19:53:00 +0200 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=91=8C=20IMPROVE:=20replace=20nested?= =?UTF-8?q?=5Fparse=5Fwith=5Ftitles,=20guard=20non-RST=20hosts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use nested_parse_to_nodes (sphinx.util.parsing) with allow_section_headings=False instead of the deprecated nested_parse_with_titles; available across sphinx>=7.4 with no version gating needed. - Warn and skip marked-RST blocks when the hosting document is not an RST file, preventing silent misparse in MyST (.md) hosts. --- .../sphinx_extension/directives/src_trace.py | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py b/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py index a637a16e..164f5aad 100644 --- a/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py +++ b/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py @@ -9,7 +9,7 @@ from docutils.statemachine import StringList from sphinx.util import logging from sphinx.util.docutils import SphinxDirective -from sphinx.util.nodes import nested_parse_with_titles +from sphinx.util.parsing import nested_parse_to_nodes from sphinx_needs.api import add_need # type: ignore[import-untyped] from sphinx_needs.utils import add_doc # type: ignore[import-untyped] @@ -359,7 +359,7 @@ def _render_marked_rst( """Parse a marked-RST block inline into the doctree. The RST content extracted from the source comment is parsed with - ``nested_parse_with_titles`` so the author has full control over what + ``nested_parse_to_nodes`` so the author has full control over what nodes are produced (needs, cross-references, admonitions, ...). The block is also registered in :data:`file_lineno_href.mappings` so the generated source-code page links the marker line back to the current @@ -371,6 +371,24 @@ def _render_marked_rst( :param dirs: Directory mapping used by :meth:`render_needs`. :return: The docutils nodes produced by parsing the RST block. """ + # Marked-RST blocks are parsed through the host document's state, which + # means the content is interpreted by whatever parser owns that document. + # In a MyST (.md) host the block would be parsed as Markdown — silently + # producing wrong output. Guard against this by checking the file + # extension of the hosting document; warn and skip for non-RST hosts. + host_suffix = Path(self.env.doc2path(self.env.docname)).suffix.lower() + if host_suffix != ".rst": + logger.warning( + "marked-RST block in %s (line %d) skipped: " + "the hosting document '%s' is not an RST file (%s). " + "Marked-RST blocks can only be rendered correctly in RST documents.", + marked_rst.filepath, + marked_rst.source_map["start"]["row"] + 1, + self.env.docname, + host_suffix, + ) + return [] + filepath = src_analyse.analyse_config.src_dir / marked_rst.filepath target_filepath = dirs["target_dir"] / filepath.relative_to(dirs["src_dir"]) @@ -396,7 +414,14 @@ def _render_marked_rst( ], ) - nested_parse_with_titles(self.state, string_list, container) + parsed = nested_parse_to_nodes( + self.state, + string_list, + source=source_ref, + offset=start_row, + allow_section_headings=False, + ) + container += parsed if local_url_field: # Point the source page anchor at the current document so users can