"""dir_attach, a Pelican plugin to attach files from a directory named after restructuredText file names. If your file is named ``foobar.rst``, then the following restructuredText entry: .. image:: {dir_attach}example-thumbnail.jpg :alt: An example image for demonstration. :target: {dir_attach}example.jpg Will automatically be pre-processed by this plugin to become: .. image:: {attach}../images/foobar/example-thumbnail.jpg :alt: An example image for demonstration. :target: {attach}../images/foobar/example.jpg Using ``../images/`` directory instead of ``images/`` is mandatory. This is because the current Pelican implementation will simplify the path only if the target directory is not in the same root as the current page. If ``images/`` directory was used, then the resulting path in HTML would be: foobar/images/foobar/example.jpg By using a directory outside of the page's source folder, the generated HTML path is simplified by Pelican: foobar/example.jpg """ import os import tempfile import re from pelican import signals from pelican.readers import RstReader def dirname_from_source_path(source_path): """Get the dirname to use from the .rst filename. >>> dirname_from_source_path("/tmp/foobar.rst") 'foobar' """ return os.path.splitext(os.path.basename(source_path))[0] def expand_dir_attach(content, dirname): """Expand the {dir_attach} directives to image {attach} directives. >>> src = \ ".. image:: {dir_attach}example.jpg\\n" \ " :alt: Example illustration.\\n" \ " :target: {dir_attach}example.jpg\\n" >>> expand_dir_attach(src, "foobar") '.. image:: {attach}../images/foobar/example.jpg\\n\ :alt: Example illustration.\\n\ :target: {attach}../images/foobar/example.jpg\\n' """ dest = f"{{attach}}../images/{dirname}/" # We are trying to match only restructuredText directives instead of doing # a whole content.replace to avoid to replace inside of paragraphs, etc. regexes = ( (r"^(.. \w+::) {dir_attach}(.*)$", fr"\1 {dest}\2"), (r"^(\s+:\w+:) {dir_attach}(.*)$", fr"\1 {dest}\2"), ) for match, replace in regexes: content = re.sub(match, replace, content, flags=re.MULTILINE) return content class CustomRstReader(RstReader): """A custom restructuredText reader that will pre-process source first.""" enabled = True file_extensions = ['rst'] def read(self, source_path): """Parses restructured text.""" dirname = dirname_from_source_path(source_path) # Open temporary file in "w" instead of default "w+b" in order to # use utf-8 by default. with tempfile.NamedTemporaryFile(mode="w") as tmp: with open(source_path) as src: tmp.write(expand_dir_attach(src.read(), dirname)) # Force flush to disk before docutils tries to open the file # in super().read(), elsewise file may be empty. tmp.flush() return super().read(tmp.name) def add_reader(readers): """Override the .rst reader with our custom reader.""" readers.reader_classes['rst'] = CustomRstReader def register(): """Register the plugin to Pelican.""" signals.readers_init.connect(add_reader)