Compare commits

..
6 Commits
12 changed files with 165 additions and 19 deletions
+12
View File
@@ -1,6 +1,18 @@
Changelog Changelog
========= =========
0.3.1
-----
- Fix issue when ``source_suffix`` equals ``source_link_suffix``
`#29 <https://github.com/jdillard/sphinx-llms-txt/pull/29>`_
0.3.0
-----
- Use first paragraph as default for ``llms_txt_summary``
`#22 <https://github.com/jdillard/sphinx-llms-txt/pull/22>`_
0.2.4 0.2.4
----- -----
+1
View File
@@ -3,6 +3,7 @@
A Sphinx extension that generates a summary `llms.txt` file and a single combined documentation `llms-full.txt` file. A Sphinx extension that generates a summary `llms.txt` file and a single combined documentation `llms-full.txt` file.
[![PyPI version](https://img.shields.io/pypi/v/sphinx-llms-txt.svg)](https://pypi.python.org/pypi/sphinx-llms-txt) [![PyPI version](https://img.shields.io/pypi/v/sphinx-llms-txt.svg)](https://pypi.python.org/pypi/sphinx-llms-txt)
[![Conda Version](https://img.shields.io/conda/vn/conda-forge/sphinx-llms-txt.svg)](https://anaconda.org/conda-forge/sphinx-llms-txt)
[![Downloads](https://static.pepy.tech/badge/sphinx-llms-txt/month)](https://pepy.tech/project/sphinx-llms-txt) [![Downloads](https://static.pepy.tech/badge/sphinx-llms-txt/month)](https://pepy.tech/project/sphinx-llms-txt)
[![Parallel Safe](https://img.shields.io/badge/parallel%20safe-true-brightgreen)](#) [![Parallel Safe](https://img.shields.io/badge/parallel%20safe-true-brightgreen)](#)
+1 -1
View File
@@ -146,7 +146,7 @@ Integration Examples
Complete Configuration Example 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 .. code-block:: python
+5 -1
View File
@@ -81,7 +81,11 @@ html_theme = "furo"
# further. For a list of options available for each theme, see the # further. For a list of options available for each theme, see the
# documentation. # documentation.
# #
html_theme_options = {} html_theme_options = {
"source_repository": "https://github.com/jdillard/sphinx-llms-txt/",
"source_branch": "main",
"source_directory": "docs/source/",
}
html_baseurl = "https://sphinx-llms-txt.readthedocs.org/" html_baseurl = "https://sphinx-llms-txt.readthedocs.org/"
+2 -2
View File
@@ -67,8 +67,8 @@ Project Configuration Values
.. confval:: llms_txt_summary .. confval:: llms_txt_summary
- **Type**: string or ``None`` - **Type**: string
- **Default**: ``None`` - **Default**: The first paragraph in the root document, else an empty string
- **Description**: Optional, but recommended, summary description for ``llms.txt``. - **Description**: Optional, but recommended, summary description for ``llms.txt``.
See :ref:`custom_summary`. See :ref:`custom_summary`.
+1 -1
View File
@@ -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**. See :doc:`advanced-configuration` for more information about how to use **sphinx-llms-txt**.
How It Works How It Works
----------- ------------
During the Sphinx build process: During the Sphinx build process:
+4 -1
View File
@@ -3,7 +3,7 @@ Sphinx llms.txt Generator
A `Sphinx`_ extension that generates a summary ``llms.txt`` file, written in Markdown, and a single combined documentation ``llms-full.txt`` file, written in reStructuredText. A `Sphinx`_ extension that generates a summary ``llms.txt`` file, written in Markdown, and a single combined documentation ``llms-full.txt`` file, written in reStructuredText.
|PyPI version| |Downloads| |Parallel Safe| |GitHub Stars| |PyPI version| |Conda Version| |Downloads| |Parallel Safe| |GitHub Stars|
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
@@ -20,6 +20,9 @@ A `Sphinx`_ extension that generates a summary ``llms.txt`` file, written in Mar
.. |PyPI version| image:: https://img.shields.io/pypi/v/sphinx-llms-txt.svg .. |PyPI version| image:: https://img.shields.io/pypi/v/sphinx-llms-txt.svg
:target: https://pypi.python.org/pypi/sphinx-llms-txt :target: https://pypi.python.org/pypi/sphinx-llms-txt
:alt: Latest PyPi Version :alt: Latest PyPi Version
.. |Conda Version| image:: https://img.shields.io/conda/vn/conda-forge/sphinx-llms-txt.svg
:target: https://anaconda.org/conda-forge/sphinx-llms-txt
:alt: Latest Conda Version
.. |Downloads| image:: https://static.pepy.tech/badge/sphinx-llms-txt/month .. |Downloads| image:: https://static.pepy.tech/badge/sphinx-llms-txt/month
:target: https://pepy.tech/project/sphinx-llms-txt :target: https://pepy.tech/project/sphinx-llms-txt
:alt: PyPi Downloads per month :alt: PyPi Downloads per month
+23 -4
View File
@@ -12,7 +12,7 @@ from .manager import LLMSFullManager
from .processor import DocumentProcessor from .processor import DocumentProcessor
from .writer import FileWriter from .writer import FileWriter
__version__ = "0.2.4" __version__ = "0.3.1"
# Export classes needed by tests # Export classes needed by tests
__all__ = [ __all__ = [
@@ -25,9 +25,14 @@ __all__ = [
# Global manager instance # Global manager instance
_manager = LLMSFullManager() _manager = LLMSFullManager()
# Store root document first paragraph
_root_first_paragraph = ""
def doctree_resolved(app: Sphinx, doctree, docname: str): def doctree_resolved(app: Sphinx, doctree, docname: str):
"""Called when a docname has been resolved to a document.""" """Called when a docname has been resolved to a document."""
global _root_first_paragraph
# Extract title from the document # Extract title from the document
title = None title = None
# findall() returns a generator, convert to list to check if it has elements # 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: if title:
_manager.update_page_title(docname, 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): def build_finished(app: Sphinx, exception):
"""Called when the build is finished.""" """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_master_doc(app.config.master_doc)
_manager.set_app(app) _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 # Set up configuration
config = { config = {
"llms_txt_file": app.config.llms_txt_file, "llms_txt_file": app.config.llms_txt_file,
"llms_txt_filename": app.config.llms_txt_filename, "llms_txt_filename": app.config.llms_txt_filename,
"llms_txt_title": app.config.llms_txt_title, "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_file": app.config.llms_txt_full_file,
"llms_txt_full_filename": app.config.llms_txt_full_filename, "llms_txt_full_filename": app.config.llms_txt_full_filename,
"llms_txt_full_max_size": app.config.llms_txt_full_max_size, "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("doctree-resolved", doctree_resolved)
app.connect("build-finished", build_finished) app.connect("build-finished", build_finished)
# Reset manager for each build # Reset manager and root paragraph for each build
global _manager global _manager, _root_first_paragraph
_manager = LLMSFullManager() _manager = LLMSFullManager()
_root_first_paragraph = ""
return { return {
"version": __version__, "version": __version__,
+7 -1
View File
@@ -90,7 +90,13 @@ class DocumentCollector:
# Try to find the source file with any of the valid source suffixes # Try to find the source file with any of the valid source suffixes
for src_suffix in source_suffixes: for src_suffix in source_suffixes:
candidate_file = sources_dir / f"{docname}{src_suffix}{source_link_suffix}" # Avoid duplicate extensions when source_suffix == source_link_suffix
if src_suffix == source_link_suffix:
candidate_file = sources_dir / f"{docname}{src_suffix}"
else:
candidate_file = (
sources_dir / f"{docname}{src_suffix}{source_link_suffix}"
)
if candidate_file.exists(): if candidate_file.exists():
return src_suffix return src_suffix
+20 -2
View File
@@ -138,13 +138,22 @@ class LLMSFullManager:
# Build the source file path directly using the known suffix # Build the source file path directly using the known suffix
if src_suffix: if src_suffix:
source_file = sources_dir / f"{docname}{src_suffix}{source_link_suffix}" # Avoid duplicate extensions when source_suffix == source_link_suffix
if src_suffix == source_link_suffix:
source_file = sources_dir / f"{docname}{src_suffix}"
expected_suffix = src_suffix
else:
source_file = (
sources_dir / f"{docname}{src_suffix}{source_link_suffix}"
)
expected_suffix = f"{src_suffix}{source_link_suffix}"
if source_file.exists(): if source_file.exists():
docname_to_file[docname] = source_file docname_to_file[docname] = source_file
else: else:
logger.warning( logger.warning(
f"sphinx-llms-txt: Source file not found for: {docname}." f"sphinx-llms-txt: Source file not found for: {docname}."
f"Expected: {docname}{src_suffix}{source_link_suffix}" f"Expected: {docname}{expected_suffix}"
) )
else: else:
logger.warning( logger.warning(
@@ -205,6 +214,10 @@ class LLMSFullManager:
source_suffixes = self._get_source_suffixes() source_suffixes = self._get_source_suffixes()
all_source_files = [] all_source_files = []
for src_suffix in source_suffixes: for src_suffix in source_suffixes:
# Avoid duplicate extensions when source_suffix == source_link_suffix
if src_suffix == source_link_suffix:
glob_pattern = f"**/*{src_suffix}"
else:
glob_pattern = f"**/*{src_suffix}{source_link_suffix}" glob_pattern = f"**/*{src_suffix}{source_link_suffix}"
all_source_files.extend(sources_dir.glob(glob_pattern)) all_source_files.extend(sources_dir.glob(glob_pattern))
@@ -231,7 +244,12 @@ class LLMSFullManager:
# Try each source suffix to find which one this file uses # Try each source suffix to find which one this file uses
for src_suffix in source_suffixes: for src_suffix in source_suffixes:
# Avoid duplicate extensions when suffixes match
if src_suffix == source_link_suffix:
combined_suffix = src_suffix
else:
combined_suffix = f"{src_suffix}{source_link_suffix}" combined_suffix = f"{src_suffix}{source_link_suffix}"
if rel_path.endswith(combined_suffix): if rel_path.endswith(combined_suffix):
docname = rel_path[: -len(combined_suffix)] # Remove suffix docname = rel_path[: -len(combined_suffix)] # Remove suffix
break break
+2
View File
@@ -88,6 +88,8 @@ class FileWriter:
if description: if description:
# Trim leading and trailing whitespace # Trim leading and trailing whitespace
description = description.strip() description = description.strip()
if description:
# Only add blockquote if description is not empty
# Replace newlines with newline + blockquote marker to maintain # Replace newlines with newline + blockquote marker to maintain
# blockquote formatting # blockquote formatting
description = description.replace("\n", "\n> ") description = description.replace("\n", "\n> ")
+81
View File
@@ -727,3 +727,84 @@ def test_source_suffix_detection_priority():
# RST should come before MD (due to priority in toctree processing) # RST should come before MD (due to priority in toctree processing)
assert rst_pos < md_pos, "RST content should appear before MD content" 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("<rst-doc>", 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