Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions docs/source/components/directive.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
Directive
=========

.. attention:: ``src-trace`` directive currently only supports :ref:`one-line need definition <oneline>`.

``CodeLinks`` provides ``src-trace`` directive and it can be used in the following ways:

.. code-block:: rst
Expand Down Expand Up @@ -76,3 +74,39 @@ 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 <configuration>`.

.. _marked_rst:

Marked reStructuredText
-----------------------

In addition to :ref:`one-line needs <oneline>`, the ``src-trace`` directive can
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``).

.. 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.
97 changes: 96 additions & 1 deletion src/sphinx_codelinks/sphinx_extension/directives/src_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.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]

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,
Expand Down Expand Up @@ -340,4 +342,97 @@ 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_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
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.
"""
# 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"])

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))
],
)

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
# 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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<document source="<source>">
<target anonymous="" ids="IMPL_MRST_BASIC_1" refid="IMPL_MRST_BASIC_1">
<Need classes="need need-impl" ids="IMPL_MRST_BASIC_1" refid="IMPL_MRST_BASIC_1">
<paragraph>
Body paragraph inside the marked RST block.
12 changes: 12 additions & 0 deletions tests/doc_test/marked_rst_basic/conf.py
Original file line number Diff line number Diff line change
@@ -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"
16 changes: 16 additions & 0 deletions tests/doc_test/marked_rst_basic/dummy_src.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#include <iostream>

/*
@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;
}
2 changes: 2 additions & 0 deletions tests/doc_test/marked_rst_basic/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.. src-trace::
:project: src
2 changes: 2 additions & 0 deletions tests/doc_test/marked_rst_basic/src_trace.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[codelinks.projects.src.analyse]
get_rst = true
4 changes: 4 additions & 0 deletions tests/test_src_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down