From 46c2dec254d281606c36b27206f809e5c540e440 Mon Sep 17 00:00:00 2001 From: Jared Dillard Date: Sun, 22 Jun 2025 21:00:40 -0700 Subject: [PATCH] Use first paragraph as summary by default (#22) --- CHANGELOG.rst | 6 ++ docs/source/advanced-configuration.rst | 2 +- docs/source/configuration-values.rst | 4 +- docs/source/getting-started.rst | 2 +- sphinx_llms_txt/__init__.py | 27 +++++++-- sphinx_llms_txt/writer.py | 10 ++-- tests/test_llms_txt.py | 81 ++++++++++++++++++++++++++ 7 files changed, 120 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cc5f92f..2cbf2b2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,12 @@ Changelog ========= +0.3.0 +----- + +- Use first paragraph as default for ``llms_txt_summary`` + `#22 `_ + 0.2.4 ----- diff --git a/docs/source/advanced-configuration.rst b/docs/source/advanced-configuration.rst index 2db6825..5d14bc4 100644 --- a/docs/source/advanced-configuration.rst +++ b/docs/source/advanced-configuration.rst @@ -146,7 +146,7 @@ Integration Examples Complete Configuration Example ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Here's a complete example showing multiple :ref:`configuration-values`: +Here's a complete example showing multiple :doc:`configuration-values`: .. code-block:: python diff --git a/docs/source/configuration-values.rst b/docs/source/configuration-values.rst index 6fd522b..df7e828 100644 --- a/docs/source/configuration-values.rst +++ b/docs/source/configuration-values.rst @@ -67,8 +67,8 @@ Project Configuration Values .. confval:: llms_txt_summary - - **Type**: string or ``None`` - - **Default**: ``None`` + - **Type**: string + - **Default**: The first paragraph in the root document, else an empty string - **Description**: Optional, but recommended, summary description for ``llms.txt``. See :ref:`custom_summary`. diff --git a/docs/source/getting-started.rst b/docs/source/getting-started.rst index be21513..9227df2 100644 --- a/docs/source/getting-started.rst +++ b/docs/source/getting-started.rst @@ -31,7 +31,7 @@ Once added, the extension will automatically generate the LLMs.txt files during See :doc:`advanced-configuration` for more information about how to use **sphinx-llms-txt**. How It Works ------------ +------------ During the Sphinx build process: diff --git a/sphinx_llms_txt/__init__.py b/sphinx_llms_txt/__init__.py index 160ed90..aab1c0a 100644 --- a/sphinx_llms_txt/__init__.py +++ b/sphinx_llms_txt/__init__.py @@ -12,7 +12,7 @@ from .manager import LLMSFullManager from .processor import DocumentProcessor from .writer import FileWriter -__version__ = "0.2.4" +__version__ = "0.3.0" # Export classes needed by tests __all__ = [ @@ -25,9 +25,14 @@ __all__ = [ # Global manager instance _manager = LLMSFullManager() +# Store root document first paragraph +_root_first_paragraph = "" + def doctree_resolved(app: Sphinx, doctree, docname: str): """Called when a docname has been resolved to a document.""" + global _root_first_paragraph + # Extract title from the document title = None # findall() returns a generator, convert to list to check if it has elements @@ -38,6 +43,14 @@ def doctree_resolved(app: Sphinx, doctree, docname: str): if title: _manager.update_page_title(docname, title) + # Extract first paragraph from root document + if docname == app.config.master_doc: + for node in doctree.traverse(nodes.paragraph): + first_para = node.astext() + if first_para: + _root_first_paragraph = first_para + break + def build_finished(app: Sphinx, exception): """Called when the build is finished.""" @@ -47,12 +60,17 @@ def build_finished(app: Sphinx, exception): _manager.set_master_doc(app.config.master_doc) _manager.set_app(app) + # Get the summary - use configured value or extracted first paragraph + summary = app.config.llms_txt_summary + if summary is None: + summary = _root_first_paragraph + # Set up configuration config = { "llms_txt_file": app.config.llms_txt_file, "llms_txt_filename": app.config.llms_txt_filename, "llms_txt_title": app.config.llms_txt_title, - "llms_txt_summary": app.config.llms_txt_summary, + "llms_txt_summary": summary, "llms_txt_full_file": app.config.llms_txt_full_file, "llms_txt_full_filename": app.config.llms_txt_full_filename, "llms_txt_full_max_size": app.config.llms_txt_full_max_size, @@ -91,9 +109,10 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.connect("doctree-resolved", doctree_resolved) app.connect("build-finished", build_finished) - # Reset manager for each build - global _manager + # Reset manager and root paragraph for each build + global _manager, _root_first_paragraph _manager = LLMSFullManager() + _root_first_paragraph = "" return { "version": __version__, diff --git a/sphinx_llms_txt/writer.py b/sphinx_llms_txt/writer.py index 1828bc0..721f391 100644 --- a/sphinx_llms_txt/writer.py +++ b/sphinx_llms_txt/writer.py @@ -88,10 +88,12 @@ class FileWriter: if description: # Trim leading and trailing whitespace description = description.strip() - # Replace newlines with newline + blockquote marker to maintain - # blockquote formatting - description = description.replace("\n", "\n> ") - f.write(f"> {description}\n\n") + if description: + # Only add blockquote if description is not empty + # Replace newlines with newline + blockquote marker to maintain + # blockquote formatting + description = description.replace("\n", "\n> ") + f.write(f"> {description}\n\n") f.write("## Docs\n\n") # Get base URL from config diff --git a/tests/test_llms_txt.py b/tests/test_llms_txt.py index 0c9c9b9..8a59318 100644 --- a/tests/test_llms_txt.py +++ b/tests/test_llms_txt.py @@ -727,3 +727,84 @@ def test_source_suffix_detection_priority(): # RST should come before MD (due to priority in toctree processing) assert rst_pos < md_pos, "RST content should appear before MD content" + + +def test_summary_default_uses_first_paragraph(): + """ + Test that summary defaults to first paragraph of root document when not configured. + """ + from docutils import nodes + from docutils.frontend import OptionParser + from docutils.parsers.rst import Parser + from docutils.utils import new_document + + from sphinx_llms_txt import build_finished, doctree_resolved + + # Create a proper document with settings + settings = OptionParser(components=(Parser,)).get_default_values() + doctree = new_document("", settings) + + title = nodes.title(text="Test Title") + paragraph = nodes.paragraph( + text="This is the first paragraph that should be used as summary." + ) + doctree.append(title) + doctree.append(paragraph) + + # Mock Sphinx app + class MockApp: + class Config: + master_doc = "index" + llms_txt_summary = None # Not configured + llms_txt_file = True + llms_txt_filename = "llms.txt" + llms_txt_title = None + llms_txt_full_file = True + llms_txt_full_filename = "llms-full.txt" + llms_txt_full_max_size = None + llms_txt_directives = [] + llms_txt_exclude = [] + html_baseurl = "" + + config = Config() + outdir = "/tmp/build" + srcdir = "/tmp/source" + + class Env: + titles = { + "index": type("TitleNode", (), {"astext": lambda self: "Test Title"})() + } + + env = Env() + + app = MockApp() + + # Reset the global state + import sphinx_llms_txt + + sphinx_llms_txt._root_first_paragraph = "" + + # Call doctree_resolved to extract the first paragraph + doctree_resolved(app, doctree, "index") + + # Verify the first paragraph was extracted + assert ( + sphinx_llms_txt._root_first_paragraph + == "This is the first paragraph that should be used as summary." + ) + + # Mock the manager methods to avoid actual file operations + original_combine_sources = sphinx_llms_txt._manager.combine_sources + sphinx_llms_txt._manager.combine_sources = lambda outdir, srcdir: None + + # Call build_finished and verify the summary is set correctly + build_finished(app, None) + + # Check that the summary was properly configured + assert ( + sphinx_llms_txt._manager.config["llms_txt_summary"] + == "This is the first paragraph that should be used as summary." + ) + + # Restore original method + sphinx_llms_txt._manager.combine_sources = original_combine_sources