@@ -120,6 +120,14 @@ def add_tailwind_classes(self, soup):
120120 for p in soup .find_all ('p' ):
121121 if self ._is_badge_paragraph (p ):
122122 p ['class' ] = 'flex flex-wrap gap-2 items-center justify-center mb-4'
123+ elif self ._is_image_with_caption_paragraph (p ):
124+ p ['class' ] = 'mb-4 text-center'
125+ for br in p .find_all ('br' ):
126+ br .decompose ()
127+ elif self ._is_image_only_paragraph (p ):
128+ p ['class' ] = 'mb-1'
129+ elif self ._is_image_caption_paragraph (p ):
130+ p ['class' ] = 'mt-1 mb-4 text-center text-sm leading-6 text-neutral-600 dark:text-neutral-400'
123131 else :
124132 p ['class' ] = f'mb-4 { s ["body_size" ]} leading-7 text-pretty text-justify text-neutral-800 dark:text-neutral-200'
125133
@@ -139,7 +147,7 @@ def add_tailwind_classes(self, soup):
139147 if img .parent and img .parent .name == 'a' :
140148 img ['class' ] = 'inline h-5 align-middle'
141149 else :
142- img ['class' ] = 'mx-auto my-5 w-full max-w-4xl h-auto rounded-xl border border-neutral-200 shadow-sm dark:border-neutral-700 dark:shadow-neutral-950/30'
150+ img ['class' ] = 'mx-auto my-2 w-full max-w-4xl h-auto rounded-xl border border-neutral-200 shadow-sm dark:border-neutral-700 dark:shadow-neutral-950/30'
143151
144152 for pre in soup .find_all ('pre' ):
145153 pre ['class' ] = f'my-5 overflow-x-auto rounded-xl border border-neutral-200 bg-neutral-950 p-4 { s ["pre_size" ]} leading-6 text-neutral-100 dark:border-neutral-700 dark:bg-neutral-950 dark:text-neutral-100'
@@ -154,7 +162,12 @@ def add_tailwind_classes(self, soup):
154162 strong ['class' ] = 'font-semibold text-neutral-900 dark:text-neutral-100'
155163
156164 for em in soup .find_all ('em' ):
157- em ['class' ] = 'italic text-neutral-700 dark:text-neutral-300'
165+ if em .parent and em .parent .name == 'p' and self ._is_image_with_caption_paragraph (em .parent ):
166+ em ['class' ] = 'mt-1 block text-sm italic text-neutral-600 dark:text-neutral-400'
167+ elif em .parent and em .parent .name == 'p' and self ._is_image_caption_paragraph (em .parent ):
168+ em ['class' ] = 'italic text-inherit'
169+ else :
170+ em ['class' ] = 'italic text-neutral-700 dark:text-neutral-300'
158171
159172 for a in soup .find_all ('a' ):
160173 a_cls = a .get ('class' ) or []
@@ -328,6 +341,64 @@ def _is_badge_paragraph(p):
328341 return False
329342 return True
330343
344+ @staticmethod
345+ def _is_image_only_paragraph (p ):
346+ """Return True if a paragraph contains only a single image (optionally wrapped by a link)."""
347+ children = [c for c in p .children if getattr (c , 'name' , None ) or str (c ).strip ()]
348+ if len (children ) != 1 :
349+ return False
350+ child = children [0 ]
351+ name = getattr (child , 'name' , None )
352+ if name == 'img' :
353+ return True
354+ if name == 'a' :
355+ a_children = [c for c in child .children if getattr (c , 'name' , None ) or str (c ).strip ()]
356+ return len (a_children ) == 1 and getattr (a_children [0 ], 'name' , None ) == 'img'
357+ return False
358+
359+ @staticmethod
360+ def _is_image_with_caption_paragraph (p ):
361+ """Return True if a paragraph contains an image and an emphasized caption line."""
362+ meaningful = [c for c in p .children if getattr (c , 'name' , None ) or str (c ).strip ()]
363+ names = {getattr (c , 'name' , None ) for c in meaningful if getattr (c , 'name' , None )}
364+ if not meaningful :
365+ return False
366+
367+ has_image = False
368+ for child in meaningful :
369+ name = getattr (child , 'name' , None )
370+ if name == 'img' :
371+ has_image = True
372+ break
373+ if name == 'a' :
374+ a_children = [c for c in child .children if getattr (c , 'name' , None ) or str (c ).strip ()]
375+ if len (a_children ) == 1 and getattr (a_children [0 ], 'name' , None ) == 'img' :
376+ has_image = True
377+ break
378+
379+ if not has_image or 'em' not in names :
380+ return False
381+
382+ # Allow only image/link, caption (<em>), line breaks and whitespace text nodes.
383+ for child in meaningful :
384+ name = getattr (child , 'name' , None )
385+ if name in ('img' , 'em' , 'br' ):
386+ continue
387+ if name == 'a' :
388+ a_children = [c for c in child .children if getattr (c , 'name' , None ) or str (c ).strip ()]
389+ if len (a_children ) == 1 and getattr (a_children [0 ], 'name' , None ) == 'img' :
390+ continue
391+ if not name and not str (child ).strip ():
392+ continue
393+ return False
394+ return True
395+
396+ @staticmethod
397+ def _is_image_caption_paragraph (p ):
398+ """Return True if a paragraph is an image caption rendered as only emphasized text."""
399+ children = [c for c in p .children if getattr (c , 'name' , None ) or str (c ).strip ()]
400+ return bool (children ) and all (getattr (c , 'name' , None ) == 'em' for c in children )
401+
331402 @staticmethod
332403 def _ensure_list_spacing (text ):
333404 """Insert a blank line before list/blockquote lines that immediately follow non-blank content.
0 commit comments