2222import jinja2
2323from wand .image import Image
2424
25- ANYDIR = '_any'
2625logger = logging .getLogger (__name__ )
2726
2827class Environment (jinja2 .Environment ):
28+
2929 _builder :Builder
3030 # Exclusive outdir for template filters
3131 _outdir :str
3232 # Exclusive srcdir for template filters
3333 # Actually it is a softlink link to _outdir.
3434 _srcdir :str
35+ # Same to _srcdir, but relative to Sphinx's srcdir.
36+ _reldir : str
3537
3638
3739 @classmethod
@@ -44,15 +46,36 @@ def setup(cls, app:Sphinx):
4446 @classmethod
4547 def _on_builder_inited (cls , app :Sphinx ):
4648 cls ._builder = app .builder
47- cls ._outdir = path .join (app .outdir , ANYDIR )
48- ensuredir (cls ._outdir )
49- cls ._srcdir = path .join (app .srcdir , ANYDIR )
49+
50+ # Template filters (like thumbnail_filter) may produces and new files,
51+ # they will be referenced in documents. While usually directive
52+ # (like ..image::) can only access file inside sphinx's srcdir(source/).
53+ #
54+ # So we create a dir in sphinx's outdir(_build/), and link it from srcdir,
55+ # then files can be referenced, then we won't messup the srcdir
56+ # (usually it is trakced by git), and our files can be cleaned up by
57+ # removing outdir.
58+ #
59+ # NOTE: we use builder name as suffix to avoid conflicts between multiple
60+ # builders.
61+ ANYDIR = ".any"
62+ reldir = ANYDIR + '_' + app .builder .name
63+ cls ._outdir = path .join (app .outdir , reldir )
64+ cls ._srcdir = path .join (app .srcdir , reldir )
65+ cls ._reldir = path .join ('/' , reldir ) # abspath relatived to srcdir
66+
67+ # Cleanup possible residual symlink.
5068 if path .islink (cls ._srcdir ):
5169 os .unlink (cls ._srcdir )
70+ if path .exists (cls ._srcdir ):
71+ os .remove (cls ._srcdir )
72+ # Link them.
73+ ensuredir (cls ._outdir )
5274 os .symlink (cls ._outdir , cls ._srcdir )
5375
54- logger .info (f'[any] exclusive srcdir: { cls ._srcdir } ' )
55- logger .info (f'[any] exclusive outdir: { cls ._outdir } ' )
76+ logger .debug (f'[any] srcdir: { cls ._srcdir } ' )
77+ logger .debug (f'[any] outdir: { cls ._outdir } ' )
78+
5679
5780 @classmethod
5881 def _on_build_finished (cls , app :Sphinx , exception ):
@@ -67,38 +90,33 @@ def __init__(self, *args, **kwargs):
6790
6891
6992 def thumbnail_filter (self , imgfn :str ) -> str :
70- imgfn = self ._ensure_rel (imgfn )
71- infn , outfn , relfn = self ._get_in_out_rel (imgfn )
72-
73- if not self ._is_outdated (outfn , infn ):
74- # No need to make thumbnail
75- return relfn
76-
77- with Image (filename = infn ) as img :
78- # Remove any associated profiles
79- img .thumbnail ()
80- # If larger than 640x480, fit within box, preserving aspect ratio
81- img .transform (resize = '640x480>' )
82- img .save (filename = outfn )
93+ srcfn , outfn , relfn = self ._get_src_out_rel (imgfn )
94+ if not self ._is_outdated (outfn , srcfn ):
95+ return relfn # no need to make thumbnail
96+ try :
97+ with Image (filename = srcfn ) as img :
98+ # Remove any associated profiles
99+ img .thumbnail ()
100+ # If larger than 640x480, fit within box, preserving aspect ratio
101+ img .transform (resize = '640x480>' )
102+ img .save (filename = outfn )
103+ except Exception as e :
104+ logger .warning ('failed to create thumbnail for %s: %s' , imgfn , e )
83105 return relfn
84106
85107
86108 def install_filter (self , fn :str ) -> str :
87109 """
88110 Install file to sphinx outdir, return the relative uri of current docname.
89111 """
90-
91- fn = self ._ensure_rel (fn )
92- src = path .join (self ._builder .srcdir , fn )
93- target = path .join (self ._builder .outdir , ANYDIR , fn )
94-
95- if not self ._is_outdated (target , src ):
96- # No need to install file
97- return relfn
98-
99- ensuredir (path .dirname (target ))
100- shutil .copy (src , target )
101- return self ._relative_uri (ANYDIR , fn )
112+ srcfn , outfn , relfn = self ._get_src_out_rel (fn )
113+ if not self ._is_outdated (outfn , srcfn ):
114+ return relfn # no need to install file
115+ try :
116+ shutil .copy (srcfn , outfn )
117+ except Exception as e :
118+ logger .warning ('failed to install %s: %s' , fn , e )
119+ return relfn
102120
103121
104122 def watermark_filter (self , imgfn :str ) -> str :
@@ -119,26 +137,31 @@ def _relative_uri(self, *args):
119137 return relative_uri (base , posixpath .join (* args ))
120138
121139
122- def _get_in_out_rel (self , fn :str ) -> tuple [str ,str ,str ]:
123- # The pass-in filenames must be relative
124- assert not path .isabs (fn )
140+ def _get_src_out_rel (self , fn :str ) -> tuple [str ,str ,str ]:
141+ """Return three paths (srcfn, outfn, relfn).
142+ :srcfn: abs path of fn, must inside sphinx's srcdir
143+ :outfn: abs path to motified file, must inside self._srcdir
144+ :relfn: path to outfn relatived to sphinx's srcdir
145+ """
146+ isabs = path .isabs (fn )
147+ if isabs :
148+ fn = fn [1 :] # skip os.sep so that it can be join
149+ else :
150+ docname = self ._builder .env .docname
151+ a , b = self ._builder .env .relfn2path (fn , docname )
152+ fn = a
125153
126- infn = path .join (self ._builder .srcdir , fn )
127- if infn .startswith (self ._srcdir ):
154+ srcfn = path .join (self ._builder .srcdir , fn )
155+ if srcfn .startswith (self ._srcdir ):
128156 # fn is outputted by other filters
129- outfn = infn
157+ outfn = srcfn
158+ relfn = path .join ('/' , fn )
130159 else :
131- # fn is specified by user
132- outfn = path .join (self ._srcdir , fn )
133- # Make sure output dir exists
134- ensuredir (path .dirname (outfn ))
135- relfn = self ._relative_uri (ANYDIR , fn )
136- return (infn , outfn , relfn )
137-
138-
139- def _ensure_rel (self , fn : str ) -> str :
140- """Convert site-wide absoulte path to relative path."""
141- return path .relpath (fn , '/' ) if path .isabs (fn ) else fn
160+ outfn = path .join (self ._srcdir , fn ) # fn is specified by user
161+ relfn = path .join (self ._reldir , fn )
162+ ensuredir (path .dirname (outfn )) # make sure output dir exists
163+ logger .debug ('[any] srcfn: %s, outfn: %s, relfn: %s' , srcfn , outfn , relfn )
164+ return (srcfn , outfn , relfn )
142165
143166
144167 def _is_outdated (self , target :str , src : str ) -> bool :
0 commit comments