1212from django .utils .translation import gettext_lazy as _
1313from django_hosts .resolvers import get_host , reverse , reverse_host
1414from docutils .core import publish_parts
15- from markdown import markdown
15+ from docutils .nodes import document
16+ from docutils .writers .html4css1 import HTMLTranslator , Writer
17+
18+ from markdown import markdown , Markdown
1619from markdown .extensions .toc import TocExtension , slugify as _md_title_slugify
20+ import markdown .treeprocessors
21+ import xml .etree .ElementTree as etree
1722
1823BLOG_DOCUTILS_SETTINGS = {
1924 "doctitle_xform" : False ,
@@ -37,6 +42,36 @@ def published(self):
3742 def active (self ):
3843 return self .filter (is_active = True )
3944
45+ _IMG_LAZY_ATTRIBUTES = {"loading" :"lazy" }
46+
47+ class LazyImageHTMLTranslator (HTMLTranslator ):
48+ """Alter the img tags to include the lazy attribute."""
49+ def __init__ (self , document : document , img_attributes : dict [str ,str ]| None = None ) -> None :
50+ super ().__init__ (document )
51+ self ._img_attributes = img_attributes or _IMG_LAZY_ATTRIBUTES
52+
53+ def emptytag (self , node , tagname , suffix = '\n ' , ** attributes ):
54+ """Construct and return an XML-compatible empty tag."""
55+ if tagname == "img" :
56+ attributes .update (self ._img_attributes )
57+ return super ().emptytag (node ,tagname ,suffix ,** attributes )
58+
59+ class LazyImageTreeprocessor (markdown .treeprocessors .Treeprocessor ):
60+ """
61+ `Treeprocessor`s are run on the `ElementTree` object before serialization.
62+
63+ This processor will add loading=lazy attribute on img tags
64+
65+ """
66+ def __init__ (self , img_attributes : dict [str ,str ]| None = None , md : Markdown | None = None ) -> None :
67+ super ().__init__ (md )
68+ self ._img_attributes = img_attributes or _IMG_LAZY_ATTRIBUTES
69+
70+ def run (self , root : etree .Element ) -> etree .Element | None :
71+ """Alter img tags with the supplemental attributes."""
72+ for img_elem in root .iter ('img' ):
73+ img_elem .attrib .update (self ._img_attributes )
74+
4075
4176class ContentFormat (models .TextChoices ):
4277 REST = "reST" , "reStructuredText"
@@ -51,20 +86,24 @@ def to_html(cls, fmt, source):
5186 if not fmt or fmt == cls .HTML :
5287 return source
5388 if fmt == cls .REST :
89+ writer = Writer ()
90+ writer .translator_class = LazyImageHTMLTranslator
91+
5492 return publish_parts (
5593 source = source ,
56- writer_name = "html" ,
94+ writer = writer ,
5795 settings_overrides = BLOG_DOCUTILS_SETTINGS ,
5896 )["fragment" ]
5997 if fmt == cls .MARKDOWN :
60- return markdown (
61- source ,
98+ md = Markdown (
6299 output_format = "html" ,
63100 extensions = [
64101 # baselevel matches `initial_header_level` from BLOG_DOCUTILS_SETTINGS
65102 TocExtension (baselevel = 3 , slugify = _md_slugify ),
66103 ],
67104 )
105+ md .treeprocessors .register (LazyImageTreeprocessor (),"lazyimage" ,0.3 )
106+ return md .convert (source )
68107 raise ValueError (f"Unsupported format { fmt } " )
69108
70109 def img (self , url , alt_text ):
0 commit comments