Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions doc/sphinx/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,24 @@ $(SPHINX_TARGETS):

.PHONY: copy-images
copy-images:
@echo "[IMAGE_COPY_DEBUG] ===== Starting image copy process ====="
@echo "[IMAGE_COPY_DEBUG] Copying images from ../assets/img/ to source/_images/"
mkdir -p source/_images
@ls -la ../assets/img/*.png 2>/dev/null | grep -E "MOLE_pillars|MOLE_OSE_circles" || echo "[IMAGE_COPY_DEBUG] Source images not found"
cp -r ../assets/img/* source/_images/ || true
@echo "[IMAGE_COPY_DEBUG] Images copied to source/_images/:"
@ls -la source/_images/*.png 2>/dev/null | grep -E "MOLE_pillars|MOLE_OSE_circles" || echo "[IMAGE_COPY_DEBUG] Images not in source/_images/"
# Copy README images to the _images directory for proper reference
@echo "[IMAGE_COPY_DEBUG] Copying images to source/intros/doc/assets/img/"
mkdir -p source/intros/doc/assets/img
cp -r ../assets/img/* source/intros/doc/assets/img/ || true
@echo "[IMAGE_COPY_DEBUG] Images copied to source/intros/doc/assets/img/:"
@ls -la source/intros/doc/assets/img/*.png 2>/dev/null | grep -E "MOLE_pillars|MOLE_OSE_circles" || echo "[IMAGE_COPY_DEBUG] Images not in source/intros/doc/assets/img/"
# Create figure directories for SVG files
mkdir -p source/api/examples/md/figures
mkdir -p source/api/examples-m/md/figures
mkdir -p source/math_functions/figures
@echo "[IMAGE_COPY_DEBUG] ===== Image copy process complete ====="

.PHONY: clean
clean:
Expand Down
147 changes: 147 additions & 0 deletions doc/sphinx/source/_ext/image_path_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"""
Sphinx extension to log image path resolution for debugging image inclusion issues.

This extension hooks into Sphinx's document reading and image processing to log:
1. When documents with includes are processed
2. Image references found in markdown files
3. Path resolution attempts
4. Final image paths used by Sphinx
"""
import os
import logging
from pathlib import Path
from docutils import nodes
from sphinx.util import logging as sphinx_logging

logger = sphinx_logging.getLogger(__name__)

def log_document_read(app, doctree):
"""Log when a document is read and what images it contains."""
docname = app.env.docname

# Get the source file path
source_file = app.env.doc2path(docname, base=None)

# Check if this is an OSE organization wrapper document
if 'ose_organization' in docname:
logger.info("=" * 80)
logger.info(f"[IMAGE_DEBUG] Processing document: {docname}")
logger.info(f"[IMAGE_DEBUG] Source file: {source_file}")
logger.info(f"[IMAGE_DEBUG] Source directory: {os.path.dirname(source_file)}")

# Check for image nodes
for node in doctree.traverse(nodes.image):
uri = node.get('uri', 'NO_URI')
logger.info(f"[IMAGE_DEBUG] Found image node with URI: {uri}")

# Try to resolve the full path
if hasattr(app.env, 'relfn2path'):
try:
rel_fn, abs_fn = app.env.relfn2path(uri, docname)
logger.info(f"[IMAGE_DEBUG] Resolved relative path: {rel_fn}")
logger.info(f"[IMAGE_DEBUG] Resolved absolute path: {abs_fn}")
logger.info(f"[IMAGE_DEBUG] File exists: {os.path.exists(abs_fn)}")
except Exception as e:
logger.warning(f"[IMAGE_DEBUG] Failed to resolve path: {e}")

logger.info("=" * 80)


def log_missing_reference(app, env, node, contnode):
"""Log missing reference attempts (including images)."""
if isinstance(node, nodes.image):
uri = node.get('uri', 'UNKNOWN')
logger.warning(f"[IMAGE_DEBUG] Missing image reference: {uri}")
logger.warning(f"[IMAGE_DEBUG] Current docname: {env.docname}")
logger.warning(f"[IMAGE_DEBUG] Current doc path: {env.doc2path(env.docname)}")
return None


def log_image_copying(app, exception):
"""Log image copying at the end of the build."""
if exception is not None:
return

logger.info("=" * 80)
logger.info("[IMAGE_DEBUG] Build finished - checking copied images")

# Check what images ended up in _images
build_images_dir = Path(app.outdir) / "_images"
if build_images_dir.exists():
logger.info(f"[IMAGE_DEBUG] Images directory: {build_images_dir}")
logger.info(f"[IMAGE_DEBUG] Images in _images directory:")
for img in sorted(build_images_dir.glob("*.png")):
logger.info(f"[IMAGE_DEBUG] - {img.name} ({img.stat().st_size} bytes)")
else:
logger.warning(f"[IMAGE_DEBUG] Images directory does not exist: {build_images_dir}")

logger.info("=" * 80)


def trace_myst_include_processing(app, docname, source):
"""
Hook into source-read event to log MyST include processing.
This runs BEFORE MyST parses the document.
"""
if 'ose_organization' in docname:
logger.info("=" * 80)
logger.info(f"[IMAGE_DEBUG] Source-read event for: {docname}")

# Check if this file has an include directive
if '{include}' in source[0]:
logger.info(f"[IMAGE_DEBUG] Document contains include directive")

# Extract the included file path
import re
include_match = re.search(r'\{include\}\s+([^\s\n]+)', source[0])
if include_match:
included_path = include_match.group(1)
logger.info(f"[IMAGE_DEBUG] Including file: {included_path}")

# Resolve the full path
source_dir = Path(app.env.doc2path(docname, base=None)).parent
full_included_path = (source_dir / included_path).resolve()
logger.info(f"[IMAGE_DEBUG] Source document dir: {source_dir}")
logger.info(f"[IMAGE_DEBUG] Resolved include path: {full_included_path}")
logger.info(f"[IMAGE_DEBUG] Include file exists: {full_included_path.exists()}")

if full_included_path.exists():
# Read the included file and look for image references
with open(full_included_path, 'r') as f:
included_content = f.read()

# Find markdown image references
image_matches = re.findall(r'!\[([^\]]*)\]\(([^)]+)\)', included_content)
if image_matches:
logger.info(f"[IMAGE_DEBUG] Found {len(image_matches)} image(s) in included file:")
for alt_text, img_path in image_matches:
logger.info(f"[IMAGE_DEBUG] - Alt: '{alt_text}', Path: '{img_path}'")

# Check if image path is relative to included file or source file
# Path relative to included file's location
rel_to_included = (full_included_path.parent / img_path).resolve()
logger.info(f"[IMAGE_DEBUG] Relative to included file: {rel_to_included}")
logger.info(f"[IMAGE_DEBUG] Exists (rel to included): {rel_to_included.exists()}")

# Path relative to source document
rel_to_source = (source_dir / img_path).resolve()
logger.info(f"[IMAGE_DEBUG] Relative to source doc: {rel_to_source}")
logger.info(f"[IMAGE_DEBUG] Exists (rel to source): {rel_to_source.exists()}")

logger.info("=" * 80)


def setup(app):
"""Register the extension."""
# Connect to various events
app.connect('doctree-read', log_document_read)
app.connect('missing-reference', log_missing_reference)
app.connect('build-finished', log_image_copying)
app.connect('source-read', trace_myst_include_processing)

return {
'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
}

99 changes: 99 additions & 0 deletions doc/sphinx/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
'matlab_args_fix', # Fix MATLAB function argument warnings
'generate_sitemap', # Generate sitemap.xml for SEO
'github_contributors', # Display GitHub contributors
'image_path_logger', # Debug image path resolution for included markdown files
]

#------------------------------------------------------------------------------
Expand Down Expand Up @@ -371,10 +372,108 @@ def fix_math_environments(app, docname, source):

source[0] = src

def copy_governance_images(app, config):
"""
Copy governance and organization images before Sphinx build starts.

This ensures images referenced in OSE_ORGANIZATION.md are available
when Sphinx processes the document, regardless of whether the build
is run via Makefile or directly via sphinx-build (e.g., on ReadTheDocs).

The images are copied to two locations:
1. source/_images/ - Standard Sphinx image directory
2. source/intros/doc/assets/img/ - Where MyST resolves relative paths
from the including file (ose_organization_wrapper.md)

See: GitHub Issue #222
"""
import shutil
from pathlib import Path
from sphinx.util import logging

logger = logging.getLogger(__name__)

# Determine paths relative to conf.py location
conf_dir = Path(app.confdir)

# Source: doc/assets/img/ (relative to repo root)
# Try multiple possible paths to handle different build environments
possible_sources = [
conf_dir.parent.parent.parent / "doc" / "assets" / "img", # Standard structure
conf_dir.parent.parent / "assets" / "img", # Alternative structure
Path("doc/assets/img").resolve(), # Absolute from current working directory
]

img_source = None
for source in possible_sources:
if source.exists():
img_source = source
break

if img_source is None:
logger.warning(
f"Image source directory not found. Tried: {[str(s) for s in possible_sources]}. "
f"Images may not display correctly. Current confdir: {conf_dir}"
)
return

logger.info(f"Copying governance images from: {img_source}")

# Destinations
img_dest_1 = conf_dir / "_images"
img_dest_2 = conf_dir / "intros" / "doc" / "assets" / "img"

destinations = [
(img_dest_1, "source/_images/"),
(img_dest_2, "source/intros/doc/assets/img/"),
]

copied_files = []
failed_files = []

# Copy to both locations
for dest_path, dest_name in destinations:
try:
dest_path.mkdir(parents=True, exist_ok=True)

# Copy all image files
for pattern in ["*.png", "*.jpg", "*.jpeg", "*.gif", "*.svg"]:
for img in img_source.glob(pattern):
dest_file = dest_path / img.name
try:
shutil.copy2(img, dest_file)
copied_files.append((img.name, dest_name))
except Exception as e:
failed_files.append((img.name, dest_name, str(e)))
logger.warning(f"Could not copy {img.name} to {dest_name}: {e}")
except Exception as e:
logger.error(f"Failed to create destination directory {dest_name}: {e}")

# Log summary
if copied_files:
logger.info(f"Successfully copied {len(set(f[0] for f in copied_files))} image(s) to required locations")
if failed_files:
logger.warning(f"Failed to copy {len(failed_files)} image(s). Check warnings above for details.")

# Verify critical images were copied (for debugging)
critical_images = ["MOLE_pillars.png", "MOLE_OSE_circles.png"]
for img_name in critical_images:
found_in_dest1 = (img_dest_1 / img_name).exists()
found_in_dest2 = (img_dest_2 / img_name).exists()
if not (found_in_dest1 and found_in_dest2):
logger.warning(
f"Critical image {img_name} may be missing. "
f"Found in _images/: {found_in_dest1}, "
f"Found in intros/doc/assets/img/: {found_in_dest2}"
)

def setup(app):
"""Setup function for Sphinx extension."""
app.add_js_file('mathconf.js')

# Copy governance images before build starts (Fix for Issue #222)
app.connect('config-inited', copy_governance_images)

# Add capability to replace problematic math environments
app.connect('source-read', fix_math_environments)

Expand Down
Loading