1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
# SPDX-FileCopyrightText: 2022 Agathe Porte <microjoe@microjoe.org>
#
# SPDX-License-Identifier: MIT
"""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"^(.. _.*:) {dir_attach}(.*)$", fr"\1 {dest}\2"),
(r"^( :\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)
|