Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c816fb1ea8 | ||
|
|
92f810592e | ||
|
|
ab0eb1dd29 | ||
|
|
57f716b2f9 | ||
|
|
236822885e | ||
|
|
46c2dec254 |
@@ -1,6 +1,18 @@
|
||||
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
|
||||
-----
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
A Sphinx extension that generates a summary `llms.txt` file and a single combined documentation `llms-full.txt` file.
|
||||
|
||||
[](https://pypi.python.org/pypi/sphinx-llms-txt)
|
||||
[](https://anaconda.org/conda-forge/sphinx-llms-txt)
|
||||
[](https://pepy.tech/project/sphinx-llms-txt)
|
||||
[](#)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+5
-1
@@ -81,7 +81,11 @@ html_theme = "furo"
|
||||
# further. For a list of options available for each theme, see the
|
||||
# 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/"
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|PyPI version| |Downloads| |Parallel Safe| |GitHub Stars|
|
||||
|PyPI version| |Conda Version| |Downloads| |Parallel Safe| |GitHub Stars|
|
||||
|
||||
.. toctree::
|
||||
: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
|
||||
:target: https://pypi.python.org/pypi/sphinx-llms-txt
|
||||
: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
|
||||
:target: https://pepy.tech/project/sphinx-llms-txt
|
||||
:alt: PyPi Downloads per month
|
||||
|
||||
@@ -12,7 +12,7 @@ from .manager import LLMSFullManager
|
||||
from .processor import DocumentProcessor
|
||||
from .writer import FileWriter
|
||||
|
||||
__version__ = "0.2.4"
|
||||
__version__ = "0.3.1"
|
||||
|
||||
# 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__,
|
||||
|
||||
@@ -90,7 +90,13 @@ class DocumentCollector:
|
||||
|
||||
# Try to find the source file with any of the valid 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():
|
||||
return src_suffix
|
||||
|
||||
|
||||
@@ -138,13 +138,22 @@ class LLMSFullManager:
|
||||
|
||||
# Build the source file path directly using the known 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():
|
||||
docname_to_file[docname] = source_file
|
||||
else:
|
||||
logger.warning(
|
||||
f"sphinx-llms-txt: Source file not found for: {docname}."
|
||||
f"Expected: {docname}{src_suffix}{source_link_suffix}"
|
||||
f"Expected: {docname}{expected_suffix}"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
@@ -205,7 +214,11 @@ class LLMSFullManager:
|
||||
source_suffixes = self._get_source_suffixes()
|
||||
all_source_files = []
|
||||
for src_suffix in source_suffixes:
|
||||
glob_pattern = f"**/*{src_suffix}{source_link_suffix}"
|
||||
# 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}"
|
||||
all_source_files.extend(sources_dir.glob(glob_pattern))
|
||||
|
||||
processed_paths = set(file.resolve() for file in docname_to_file.values())
|
||||
@@ -231,7 +244,12 @@ class LLMSFullManager:
|
||||
|
||||
# Try each source suffix to find which one this file uses
|
||||
for src_suffix in source_suffixes:
|
||||
combined_suffix = f"{src_suffix}{source_link_suffix}"
|
||||
# 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}"
|
||||
|
||||
if rel_path.endswith(combined_suffix):
|
||||
docname = rel_path[: -len(combined_suffix)] # Remove suffix
|
||||
break
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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("<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
|
||||
|
||||
Reference in New Issue
Block a user