Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
368c349d58 | ||
|
|
c816fb1ea8 | ||
|
|
92f810592e | ||
|
|
ab0eb1dd29 | ||
|
|
57f716b2f9 | ||
|
|
236822885e | ||
|
|
46c2dec254 |
@@ -1,6 +1,25 @@
|
|||||||
Changelog
|
Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
0.3.2
|
||||||
|
-----
|
||||||
|
|
||||||
|
- Fix image paths to deployed images
|
||||||
|
`#30 <https://github.com/jdillard/sphinx-llms-txt/pull/30>`_
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
[](https://pypi.python.org/pypi/sphinx-llms-txt)
|
[](https://pypi.python.org/pypi/sphinx-llms-txt)
|
||||||
|
[](https://anaconda.org/conda-forge/sphinx-llms-txt)
|
||||||
[](https://pepy.tech/project/sphinx-llms-txt)
|
[](https://pepy.tech/project/sphinx-llms-txt)
|
||||||
[](#)
|
[](#)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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/"
|
||||||
|
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.2"
|
||||||
|
|
||||||
# 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__,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -94,8 +94,14 @@ class DocumentProcessor:
|
|||||||
if not base_url:
|
if not base_url:
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
# Ensure base URL ends with slash
|
||||||
if not base_url.endswith("/"):
|
if not base_url.endswith("/"):
|
||||||
base_url += "/"
|
base_url += "/"
|
||||||
|
|
||||||
|
# Remove leading slash from path to avoid double slashes
|
||||||
|
if path.startswith("/"):
|
||||||
|
path = path[1:]
|
||||||
|
|
||||||
return f"{base_url}{path}"
|
return f"{base_url}{path}"
|
||||||
|
|
||||||
def _is_absolute_or_url(self, path: str) -> bool:
|
def _is_absolute_or_url(self, path: str) -> bool:
|
||||||
@@ -137,8 +143,41 @@ class DocumentProcessor:
|
|||||||
prefix = match.group(1) # The entire directive prefix including whitespace
|
prefix = match.group(1) # The entire directive prefix including whitespace
|
||||||
path = match.group(3).strip() # The path argument
|
path = match.group(3).strip() # The path argument
|
||||||
|
|
||||||
# Only process relative paths, not absolute paths or URLs
|
# Handle URLs and data URIs - leave unchanged
|
||||||
if not self._is_absolute_or_url(path):
|
if path.startswith(("http://", "https://", "data:")):
|
||||||
|
return match.group(0)
|
||||||
|
|
||||||
|
# For ALL paths, check if image exists in _images first
|
||||||
|
# Extract filename from the path
|
||||||
|
filename = os.path.basename(path)
|
||||||
|
|
||||||
|
# Check if image exists in _images directory
|
||||||
|
# First determine the build directory from source_path
|
||||||
|
build_dir = None
|
||||||
|
if "_sources" in str(source_path):
|
||||||
|
# Extract build directory (parent of _sources)
|
||||||
|
path_parts = str(source_path).split("_sources/")
|
||||||
|
if len(path_parts) > 1:
|
||||||
|
build_dir = path_parts[0].rstrip("/")
|
||||||
|
|
||||||
|
# If we can determine the build directory, check if image exists in _images
|
||||||
|
if build_dir:
|
||||||
|
images_path = os.path.join(build_dir, "_images", filename)
|
||||||
|
if os.path.exists(images_path):
|
||||||
|
# Image exists in _images, use _images path
|
||||||
|
full_path = f"/_images/{filename}"
|
||||||
|
# Add base URL if configured
|
||||||
|
full_path = self._add_base_url(full_path, base_url)
|
||||||
|
return f"{prefix}{full_path}"
|
||||||
|
|
||||||
|
# Image doesn't exist in _images, handle based on path type
|
||||||
|
# Handle absolute paths (starting with /) - add base URL if configured
|
||||||
|
if path.startswith("/"):
|
||||||
|
# Add base URL to absolute paths if configured
|
||||||
|
full_path = self._add_base_url(path, base_url)
|
||||||
|
return f"{prefix}{full_path}"
|
||||||
|
|
||||||
|
# Handle relative paths with original logic for backward compatibility
|
||||||
# Special case for test files
|
# Special case for test files
|
||||||
if is_test:
|
if is_test:
|
||||||
# Add subdir/ prefix to match test expectations
|
# Add subdir/ prefix to match test expectations
|
||||||
@@ -168,9 +207,7 @@ class DocumentProcessor:
|
|||||||
elif rel_doc_dir:
|
elif rel_doc_dir:
|
||||||
# Join with the original path to form full path relative
|
# Join with the original path to form full path relative
|
||||||
# to srcdir
|
# to srcdir
|
||||||
full_path = os.path.normpath(
|
full_path = os.path.normpath(os.path.join(rel_doc_dir, path))
|
||||||
os.path.join(rel_doc_dir, path)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
full_path = path
|
full_path = path
|
||||||
|
|
||||||
@@ -180,7 +217,12 @@ class DocumentProcessor:
|
|||||||
# Return the updated directive with the full path
|
# Return the updated directive with the full path
|
||||||
return f"{prefix}{full_path}"
|
return f"{prefix}{full_path}"
|
||||||
|
|
||||||
# If we couldn't resolve the path or it's already absolute, return unchanged
|
# Fallback for relative paths - add base URL if configured
|
||||||
|
else:
|
||||||
|
full_path = self._add_base_url(path, base_url)
|
||||||
|
return f"{prefix}{full_path}"
|
||||||
|
|
||||||
|
# If we couldn't resolve the path, return unchanged
|
||||||
return match.group(0)
|
return match.group(0)
|
||||||
|
|
||||||
# Replace directive paths in the content
|
# Replace directive paths in the content
|
||||||
|
|||||||
@@ -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> ")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ def test_process_path_directives_with_html_baseurl(tmp_path):
|
|||||||
|
|
||||||
|
|
||||||
def test_process_path_directives_absolute_urls(tmp_path):
|
def test_process_path_directives_absolute_urls(tmp_path):
|
||||||
"""Test that absolute URLs are not modified."""
|
"""Test that absolute URLs are not modified but absolute paths get base URL."""
|
||||||
# Create a processor
|
# Create a processor
|
||||||
config = {
|
config = {
|
||||||
"llms_txt_directives": [],
|
"llms_txt_directives": [],
|
||||||
@@ -127,10 +127,17 @@ def test_process_path_directives_absolute_urls(tmp_path):
|
|||||||
with open(source_file, "w", encoding="utf-8") as f:
|
with open(source_file, "w", encoding="utf-8") as f:
|
||||||
f.write(source_content)
|
f.write(source_content)
|
||||||
|
|
||||||
# Process the directives (should remain unchanged)
|
# Process the directives
|
||||||
processed_content = processor._process_path_directives(source_content, source_file)
|
processed_content = processor._process_path_directives(source_content, source_file)
|
||||||
|
|
||||||
assert processed_content == source_content
|
# Expected: URLs and data URIs unchanged, absolute paths get base URL
|
||||||
|
expected_content = (
|
||||||
|
".. image:: https://othersite.com/images/test.png\n"
|
||||||
|
".. image:: https://example.com/docs/absolute/path/image.png\n"
|
||||||
|
".. image:: data:image/png;base64,iVBORw0KG...\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert processed_content == expected_content
|
||||||
|
|
||||||
|
|
||||||
def test_process_path_directives_custom_directives(tmp_path):
|
def test_process_path_directives_custom_directives(tmp_path):
|
||||||
@@ -251,3 +258,149 @@ def test_process_content_end_to_end(tmp_path):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert processed_content == expected_content
|
assert processed_content == expected_content
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_path_directives_images_directory(tmp_path):
|
||||||
|
"""Test that _images directory paths are handled correctly."""
|
||||||
|
# Create a processor with base URL
|
||||||
|
config = {
|
||||||
|
"llms_txt_directives": [],
|
||||||
|
"html_baseurl": "https://example.com/docs",
|
||||||
|
}
|
||||||
|
processor = DocumentProcessor(config)
|
||||||
|
|
||||||
|
# Create source directory structure
|
||||||
|
src_dir = tmp_path / "src"
|
||||||
|
src_dir.mkdir()
|
||||||
|
processor.srcdir = str(src_dir)
|
||||||
|
|
||||||
|
# Create _sources directory to mimic Sphinx output
|
||||||
|
build_dir = tmp_path / "build"
|
||||||
|
build_dir.mkdir()
|
||||||
|
sources_dir = build_dir / "_sources"
|
||||||
|
sources_dir.mkdir()
|
||||||
|
|
||||||
|
# Create a source file with various _images directory paths
|
||||||
|
source_content = (
|
||||||
|
"Some content.\n"
|
||||||
|
".. image:: _images/test.png\n" # Relative _images should become /_images
|
||||||
|
".. image:: /_images/absolute.png\n" # Absolute _images should get base URL
|
||||||
|
".. figure:: _images/figure.png\n" # Test with figure directive too
|
||||||
|
" :alt: A test figure\n"
|
||||||
|
".. image:: images/normal.png\n" # Normal relative path should be unchanged
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create source file in sources directory to simulate Sphinx build output
|
||||||
|
source_file = sources_dir / "page.txt"
|
||||||
|
with open(source_file, "w", encoding="utf-8") as f:
|
||||||
|
f.write(source_content)
|
||||||
|
|
||||||
|
# Process the directives
|
||||||
|
processed_content = processor._process_path_directives(source_content, source_file)
|
||||||
|
|
||||||
|
# Expected: _images paths should be converted and get base URL
|
||||||
|
expected_content = (
|
||||||
|
"Some content.\n"
|
||||||
|
".. image:: https://example.com/docs/_images/test.png\n"
|
||||||
|
".. image:: https://example.com/docs/_images/absolute.png\n"
|
||||||
|
".. figure:: https://example.com/docs/_images/figure.png\n"
|
||||||
|
" :alt: A test figure\n"
|
||||||
|
".. image:: https://example.com/docs/images/normal.png\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert processed_content == expected_content
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_path_directives_images_directory_no_baseurl(tmp_path):
|
||||||
|
"""
|
||||||
|
Test that _images directory paths work correctly without base URL.
|
||||||
|
Only converts when image exists.
|
||||||
|
"""
|
||||||
|
# Create a processor without base URL
|
||||||
|
config = {
|
||||||
|
"llms_txt_directives": [],
|
||||||
|
"html_baseurl": "",
|
||||||
|
}
|
||||||
|
processor = DocumentProcessor(config)
|
||||||
|
|
||||||
|
# Create source directory structure
|
||||||
|
src_dir = tmp_path / "src"
|
||||||
|
src_dir.mkdir()
|
||||||
|
processor.srcdir = str(src_dir)
|
||||||
|
|
||||||
|
# Create _sources directory to mimic Sphinx output
|
||||||
|
build_dir = tmp_path / "build"
|
||||||
|
build_dir.mkdir()
|
||||||
|
sources_dir = build_dir / "_sources"
|
||||||
|
sources_dir.mkdir()
|
||||||
|
|
||||||
|
# Create _images directory and one test image
|
||||||
|
images_dir = build_dir / "_images"
|
||||||
|
images_dir.mkdir()
|
||||||
|
(images_dir / "test.png").write_text("fake image content")
|
||||||
|
# Note: absolute.png is not created, so it won't be converted
|
||||||
|
|
||||||
|
# Create a source file with _images directory paths
|
||||||
|
source_content = (
|
||||||
|
".. image:: _images/test.png\n" # Should become /_images (image exists)
|
||||||
|
".. image:: /_images/absolute.png\n" # Should stay unchanged (absolute path)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create source file in sources directory to simulate Sphinx build output
|
||||||
|
source_file = sources_dir / "page.txt"
|
||||||
|
with open(source_file, "w", encoding="utf-8") as f:
|
||||||
|
f.write(source_content)
|
||||||
|
|
||||||
|
# Process the directives
|
||||||
|
processed_content = processor._process_path_directives(source_content, source_file)
|
||||||
|
|
||||||
|
# Expected: only test.png gets converted because it exists in _images
|
||||||
|
expected_content = (
|
||||||
|
".. image:: /_images/test.png\n" # Converted because image exists
|
||||||
|
".. image:: /_images/absolute.png\n" # Absolute path unchanged
|
||||||
|
)
|
||||||
|
|
||||||
|
assert processed_content == expected_content
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_path_directives_all_absolute_paths_get_baseurl(tmp_path):
|
||||||
|
"""Test that all absolute paths (starting with /) get base URL prepended."""
|
||||||
|
# Create a processor with base URL
|
||||||
|
config = {
|
||||||
|
"llms_txt_directives": [],
|
||||||
|
"html_baseurl": "https://mysite.com/docs/",
|
||||||
|
}
|
||||||
|
processor = DocumentProcessor(config)
|
||||||
|
|
||||||
|
# Create source directory structure
|
||||||
|
src_dir = tmp_path / "src"
|
||||||
|
src_dir.mkdir()
|
||||||
|
processor.srcdir = str(src_dir)
|
||||||
|
|
||||||
|
# Create a source file with various absolute paths
|
||||||
|
source_content = (
|
||||||
|
".. image:: /static/images/logo.png\n"
|
||||||
|
".. figure:: /assets/diagrams/flow.svg\n"
|
||||||
|
".. image:: /media/photos/team.jpg\n"
|
||||||
|
" :alt: Team photo\n"
|
||||||
|
".. image:: relative/path.png\n" # This should still get normal processing
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create source file
|
||||||
|
source_file = src_dir / "page.txt"
|
||||||
|
with open(source_file, "w", encoding="utf-8") as f:
|
||||||
|
f.write(source_content)
|
||||||
|
|
||||||
|
# Process the directives
|
||||||
|
processed_content = processor._process_path_directives(source_content, source_file)
|
||||||
|
|
||||||
|
# Expected: All absolute paths get base URL prepended
|
||||||
|
expected_content = (
|
||||||
|
".. image:: https://mysite.com/docs/static/images/logo.png\n"
|
||||||
|
".. figure:: https://mysite.com/docs/assets/diagrams/flow.svg\n"
|
||||||
|
".. image:: https://mysite.com/docs/media/photos/team.jpg\n"
|
||||||
|
" :alt: Team photo\n"
|
||||||
|
".. image:: https://mysite.com/docs/relative/path.png\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert processed_content == expected_content
|
||||||
|
|||||||
Reference in New Issue
Block a user