diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cfde63d..cf13efd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,13 @@ Changelog ========= +0.3.2 +----- + +- Fix image paths to deployed images + `#30 `_ + + 0.3.1 ----- diff --git a/sphinx_llms_txt/__init__.py b/sphinx_llms_txt/__init__.py index 43fe2be..f71c35c 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.3.1" +__version__ = "0.3.2" # Export classes needed by tests __all__ = [ diff --git a/sphinx_llms_txt/processor.py b/sphinx_llms_txt/processor.py index ef7024b..d935efb 100644 --- a/sphinx_llms_txt/processor.py +++ b/sphinx_llms_txt/processor.py @@ -94,8 +94,14 @@ class DocumentProcessor: if not base_url: return path + # Ensure base URL ends with slash if not base_url.endswith("/"): base_url += "/" + + # Remove leading slash from path to avoid double slashes + if path.startswith("/"): + path = path[1:] + return f"{base_url}{path}" def _is_absolute_or_url(self, path: str) -> bool: @@ -137,12 +143,73 @@ class DocumentProcessor: prefix = match.group(1) # The entire directive prefix including whitespace path = match.group(3).strip() # The path argument - # Only process relative paths, not absolute paths or URLs - if not self._is_absolute_or_url(path): - # Special case for test files - if is_test: - # Add subdir/ prefix to match test expectations - full_path = "subdir/" + path + # Handle URLs and data URIs - leave unchanged + 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 + if is_test: + # Add subdir/ prefix to match test expectations + full_path = "subdir/" + path + + # If base_url is set, prepend it to the path + full_path = self._add_base_url(full_path, base_url) + + # Return the updated directive with the full path + return f"{prefix}{full_path}" + + # Production case (not in test) + elif "_sources" in str(source_path): + # Extract the part after _sources/ + rel_doc_path, rel_doc_dir, rel_doc_path_parts = ( + self._extract_relative_document_path(source_path) + ) + + if rel_doc_path_parts: + # For test subdirectory handling - this is for our test cases + if ( + len(rel_doc_path_parts) > 0 + and rel_doc_path_parts[0] == "subdir" + ): + full_path = os.path.normpath(os.path.join("subdir", path)) + # Only add the rel_doc_dir if it's not empty + elif rel_doc_dir: + # Join with the original path to form full path relative + # to srcdir + full_path = os.path.normpath(os.path.join(rel_doc_dir, path)) + else: + full_path = path # If base_url is set, prepend it to the path full_path = self._add_base_url(full_path, base_url) @@ -150,37 +217,12 @@ class DocumentProcessor: # Return the updated directive with the full path return f"{prefix}{full_path}" - # Production case (not in test) - elif "_sources" in str(source_path): - # Extract the part after _sources/ - rel_doc_path, rel_doc_dir, rel_doc_path_parts = ( - self._extract_relative_document_path(source_path) - ) + # 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 rel_doc_path_parts: - # For test subdirectory handling - this is for our test cases - if ( - len(rel_doc_path_parts) > 0 - and rel_doc_path_parts[0] == "subdir" - ): - full_path = os.path.normpath(os.path.join("subdir", path)) - # Only add the rel_doc_dir if it's not empty - elif rel_doc_dir: - # Join with the original path to form full path relative - # to srcdir - full_path = os.path.normpath( - os.path.join(rel_doc_dir, path) - ) - else: - full_path = path - - # If base_url is set, prepend it to the path - full_path = self._add_base_url(full_path, base_url) - - # Return the updated directive with the full path - return f"{prefix}{full_path}" - - # If we couldn't resolve the path or it's already absolute, return unchanged + # If we couldn't resolve the path, return unchanged return match.group(0) # Replace directive paths in the content diff --git a/tests/test_path_directives.py b/tests/test_path_directives.py index ce72e38..7c4fb2d 100644 --- a/tests/test_path_directives.py +++ b/tests/test_path_directives.py @@ -102,7 +102,7 @@ def test_process_path_directives_with_html_baseurl(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 config = { "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: f.write(source_content) - # Process the directives (should remain unchanged) + # Process the directives 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): @@ -251,3 +258,149 @@ def test_process_content_end_to_end(tmp_path): ) 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