@@ -193,12 +193,14 @@ def _rewrite_locale_asset_paths(doctree: nodes.document) -> None:
193193 """Rewrite image paths so they work in both normal and gettext (locale) builds.
194194
195195 In locale builds, content comes from .po files; relative paths like ../images/...
196- are then resolved from the document dir (source/), so ../images points to
197- sphinx-docs/images instead of project root. Fix by rewriting ../images/ to
198- ../../images/ (relative to source/ = project root images/) .
196+ are resolved from the document dir (source/). Use ../../ images/ (2 levels up from
197+ source/ = project root). If Read the Docs resolves from locale/es/, try
198+ setting LOCALE_IMAGE_LEVELS=3 in the build environment .
199199 """
200+ # Allow override: LOCALE_IMAGE_LEVELS=3 if Read the Docs resolves from locale/es/
201+ levels = max (1 , int (os .environ .get ("LOCALE_IMAGE_LEVELS" , "3" )))
200202 prefix = '../images/'
201- replacement = ' ../../ images/'
203+ replacement = ( " ../" * levels ) + " images/"
202204 for node in doctree .traverse (nodes .image ):
203205 uri = node .get ('uri' , '' )
204206 if uri .startswith (prefix ):
@@ -216,10 +218,31 @@ def _rewrite_locale_asset_paths(doctree: nodes.document) -> None:
216218 r'^\[([^\]]*)\]\s*\(\s*([^)]+)\s*\)\s*$' ,
217219 re .DOTALL ,
218220)
219- # Link inside a paragraph: [text](url)
221+ # Link inside a paragraph: [text](url) or [text] (url) - allow space between ] and (
220222_LINK_INLINE_RE = re .compile (
221- r'\[([^\]]*)\]\(\s*([^)]+)\s*\)' ,
223+ r'\[([^\]]*)\]\s*\ (\s*([^)]+)\s*\)' ,
222224)
225+ # Inline image inside a paragraph:  - path may be ../images/...
226+ _IMAGE_INLINE_RE = re .compile (
227+ r'!\s*\[([^\]]*)\]\s*\(\s*((?:\.\./)+images/[^)]+)\s*\)' ,
228+ )
229+ _IMAGE_EXTENSIONS = ('.png' , '.jpg' , '.jpeg' , '.gif' , '.svg' , '.webp' )
230+
231+
232+ def _is_image_path (target : str ) -> bool :
233+ """Return True if target is an image path (e.g. ../images/.../file.png)."""
234+ t = target .strip ().lower ()
235+ return any (t .endswith (ext ) for ext in _IMAGE_EXTENSIONS ) or (
236+ 'images/' in t and any (ext in t for ext in _IMAGE_EXTENSIONS )
237+ )
238+
239+
240+ def _image_uri_from_path (path : str ) -> str :
241+ """Normalize image path to correct depth for locale builds."""
242+ levels = int (os .environ .get ("LOCALE_IMAGE_LEVELS" , "3" ))
243+ if "images/" in path :
244+ return ("../" * levels ) + "images/" + path .split ("images/" , 1 )[1 ]
245+ return path
223246
224247
225248def _convert_literal_markdown_paragraphs (app , doctree : nodes .document , docname : str ) -> None :
@@ -249,9 +272,10 @@ def _convert_literal_markdown_paragraphs(app, doctree: nodes.document, docname:
249272 if match :
250273 alt = match .group (1 ).strip ()
251274 path = match .group (2 ).strip ()
252- # Normalize to ../../images/... (relative to source/ = project root)
275+ # Normalize path (same logic as _rewrite_locale_asset_paths)
276+ levels = int (os .environ .get ("LOCALE_IMAGE_LEVELS" , "3" ))
253277 if "images/" in path :
254- uri = "../../ images/" + path .split ("images/" , 1 )[1 ]
278+ uri = ( "../" * levels ) + " images/" + path .split ("images/" , 1 )[1 ]
255279 else :
256280 uri = path
257281 img = nodes .image (uri = uri , alt = alt )
@@ -265,6 +289,17 @@ def _convert_literal_markdown_paragraphs(app, doctree: nodes.document, docname:
265289 if match :
266290 link_text = match .group (1 ).strip ()
267291 target = match .group (2 ).strip ()
292+ # If target is an image path, render as img not link
293+ if _is_image_path (target ):
294+ uri = _image_uri_from_path (target )
295+ from html import escape
296+ raw = nodes .raw (
297+ "" ,
298+ f'<img src="{ escape (uri )} " alt="{ escape (link_text )} " />' ,
299+ format = "html" ,
300+ )
301+ node .replace_self (raw )
302+ continue
268303 if any (target .startswith (s ) for s in ("http://" , "https://" , "mailto:" )):
269304 refuri = target
270305 elif get_uri :
@@ -288,6 +323,28 @@ def _convert_literal_markdown_paragraphs(app, doctree: nodes.document, docname:
288323 node .replace_self (raw )
289324 continue
290325
326+ # Paragraph contains inline image: "text " - render img
327+ match_img = _IMAGE_INLINE_RE .search (text )
328+ if match_img and _IMAGE_INLINE_RE .search (text , match_img .end ()) is None :
329+ from html import escape
330+ before = text [: match_img .start ()]
331+ alt = match_img .group (1 ).strip ()
332+ path = match_img .group (2 ).strip ()
333+ uri = _image_uri_from_path (path )
334+ after = text [match_img .end () :]
335+ new_children = []
336+ if before :
337+ new_children .append (nodes .Text (before ))
338+ new_children .append (
339+ nodes .raw ("" , f'<img src="{ escape (uri )} " alt="{ escape (alt )} " />' , format = "html" )
340+ )
341+ if after :
342+ new_children .append (nodes .Text (after ))
343+ node .clear ()
344+ for c in new_children :
345+ node += c
346+ continue
347+
291348 # Paragraph contains one markdown link in the middle (e.g. "Consulta la [Guía](installation.md) para...")
292349 match_inline = _LINK_INLINE_RE .search (text )
293350 if match_inline and _LINK_INLINE_RE .search (text , match_inline .end ()) is None :
@@ -297,6 +354,21 @@ def _convert_literal_markdown_paragraphs(app, doctree: nodes.document, docname:
297354 link_text = match_inline .group (1 ).strip ()
298355 target = match_inline .group (2 ).strip ()
299356 after = text [match_inline .end () :]
357+ # If target is an image path, render as img not link
358+ if _is_image_path (target ):
359+ uri = _image_uri_from_path (target )
360+ new_children = []
361+ if before :
362+ new_children .append (nodes .Text (before ))
363+ new_children .append (
364+ nodes .raw ("" , f'<img src="{ escape (uri )} " alt="{ escape (link_text )} " />' , format = "html" )
365+ )
366+ if after :
367+ new_children .append (nodes .Text (after ))
368+ node .clear ()
369+ for c in new_children :
370+ node += c
371+ continue
300372 if any (target .startswith (s ) for s in ("http://" , "https://" , "mailto:" )):
301373 refuri = target
302374 elif get_uri :
0 commit comments