boop.py 7.73 KB
Newer Older
1
2
3
#!/bin/env python3

import os
4
import sys
5
import shutil
6
import distutils.dir_util
7
import locale
berumuron's avatar
berumuron committed
8
import yaml
9
10
import datetime
import uuid
11

12
import markdown
13
import pytz
14

15
import boopsy
16
import boopfeed
17
18


19
locale.setlocale(locale.LC_ALL, "")
20

21

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class ProgramError(Exception):
    """Exception raised during the program execution.

    Examples:

    >>> try:
    ...     raise ProgramError("Oops")
    ... except ProgramError as e:
    ...     print(e.args[0])
    Oops
    """

    pass


37
class Article:
38
    def __init__(self, slug, article_content, configuration={}):
39
40
        # We convert the file's content into HTML
        md = markdown.Markdown(extensions=["meta", "fenced_code", "attr_list"])
41
        content = md.convert(article_content)
42
43
44
45

        # We get the local variables from the Markdown file (metadata)
        # which will be accessible in the article template
        self.meta = {}
46
47
        self.meta.update(configuration)
        self.meta["ARTICLE_SLUG"] = slug
48
49
50
51
52
53
54
        for key, values in md.Meta.items():
            article_key = f"ARTICLE_{key.upper()}"
            # The Markdown's meta extension extract all the values in arrays, even
            # if there is only one value. Because there is no iteration system in
            # the template system, for the moment we just get the first value of
            # the array.
            self.meta[article_key] = values[0]
55
        self.meta["ARTICLE_CONTENT"] = content
56
57
58
59
60
61

        # And we initialize a template for the article
        article_template_filepath = os.path.join("theme", "article.html")
        self.template = boopsy.Template(article_template_filepath)

    def render(self):
62
63
64
65
66
67
68
69
        return self.template.render(self.meta)

    def slug(self):
        return self.meta["ARTICLE_SLUG"]

    def title(self):
        return self.meta.get("ARTICLE_TITLE", "An article")

70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
    def author(self):
        if "ARTICLE_AUTHOR" in self.meta:
            return self.meta["ARTICLE_AUTHOR"]
        elif "SITE_AUTHOR" in self.meta:
            return self.meta["SITE_AUTHOR"]
        else:
            return None

    def url(self):
        site_url = self.meta.get("SITE_URL", "")
        return f"{site_url}{self.slug()}.html"

    def uuid(self):
        return str(uuid.uuid5(uuid.NAMESPACE_URL, self.url()))

85
86
    def content(self):
        return self.meta["ARTICLE_CONTENT"]
87

88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
    def timezone(self):
        site_timezone = self.meta.get("SITE_TIMEZONE", "UTC")
        return pytz.timezone(site_timezone)

    def date(self):
        if "ARTICLE_DATE" not in self.meta:
            return datetime.datetime.now(tz=self.timezone())

        article_date = datetime.datetime.strptime(
            self.meta["ARTICLE_DATE"], "%Y-%m-%d %H:%M"
        )
        return article_date.astimezone(tz=self.timezone())

    def update(self):
        if "ARTICLE_UPDATE" not in self.meta:
            return None

        article_update = datetime.datetime.strptime(
            self.meta["ARTICLE_UPDATE"], "%Y-%m-%d %H:%M"
        )
        return article_update.astimezone(tz=self.timezone())

110

111
112
113
114
115
116
117
118
119
120
121
122
def dir_tree(path):
    """List all the tree directory under the given path.
    """
    for filename in os.listdir(path):
        full_path = os.path.join(path, filename)
        if os.path.isdir(full_path):
            for sub_filename in dir_tree(full_path):
                yield os.path.join(filename, sub_filename)
        else:
            yield filename


123
def main():
124
    # Make sure ./site folder is empty
berumuron's avatar
berumuron committed
125
    output_path = os.path.join(os.curdir, "site")
126
    if os.path.isdir(output_path):
127
        # it should be a directory...
128
        shutil.rmtree(output_path)
129
130
131
    elif os.path.exists(output_path):
        # ... but it also can be a simple file if user created it manually!
        os.remove(output_path)
132
    os.mkdir(output_path)
133
134
135
136
137
138
139
140

    # Check that index file exists and copy it under the ./site folder
    index_filepath = os.path.join(os.curdir, "index.html")
    if not os.path.exists(index_filepath):
        raise ProgramError("./index.html file does not exist")
    index_output_filepath = os.path.join(output_path, "index.html")
    shutil.copyfile(index_filepath, index_output_filepath)

141
142
143
    # Check that a ./static folder exists and copy its content under the ./site
    # folder
    static_dirpath = os.path.join(os.curdir, "static")
144
145
146
    if os.path.isdir(static_dirpath):
        distutils.dir_util.copy_tree(static_dirpath, output_path)

147
148
149
    # Check that articles directory exists on the filesystem
    articles_path = os.path.join(os.curdir, "articles")
    if not os.path.isdir(articles_path):
150
151
        # If not, just stop here
        return
152

berumuron's avatar
berumuron committed
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
    # Load the configuration (it can be overidden by a configuration.yml file)
    configuration = {
        "SITE_URL": "http://localhost",
        "SITE_TITLE": "A website",
        "SITE_TIMEZONE": "UTC",
    }

    configuration_path = os.path.join(os.curdir, "configuration.yml")
    if os.path.exists(configuration_path):
        with open(configuration_path) as conf_file:
            conf_data = yaml.load(conf_file)
        for key, value in conf_data.items():
            site_key = f"SITE_{key.upper()}"
            configuration[site_key] = value

168
169
170
    configuration["SITE_UUID"] = str(
        uuid.uuid5(uuid.NAMESPACE_URL, configuration["SITE_URL"])
    )
berumuron's avatar
berumuron committed
171
172
173
    if "SITE_AUTHOR" not in configuration:
        configuration["SITE_AUTHOR"] = f"{configuration['SITE_TITLE']}'s author"

174
175
    # List files from ./articles
    articles = []
176
    for filename in dir_tree(articles_path):
177
        slug, ext = os.path.splitext(filename)
178
179
        if ext != ".md":
            # Don't consider files which are not Markdown
180
            continue
181

182
        # We read the article file and initialize an Article from it
183
        article_filepath = os.path.join(articles_path, filename)
184
        with open(article_filepath, "r") as article_file:
berumuron's avatar
berumuron committed
185
            article = Article(slug, article_file.read(), configuration=configuration)
186
            articles.append(article)
187

188
189
190
191
    # If there is no articles, stop there
    if len(articles) == 0:
        return

192
193
    # And write them in the ./site folder
    for article in articles:
194
        output_filepath = os.path.join(output_path, f"{article.slug()}.html")
195
        with open(output_filepath, "w") as output_file:
196
            output_file.write(article.render())
197

198
199
200
201
202
203
204
205
    # Just create some variables so it's easier to manipulate after
    site_url = configuration["SITE_URL"]
    site_uuid = configuration["SITE_UUID"]
    site_title = configuration["SITE_TITLE"]
    site_timezone = configuration["SITE_TIMEZONE"]
    timezone = pytz.timezone(site_timezone)
    current_datetime = datetime.datetime.now(tz=timezone)

206
207
208
209
210
211
212
213
214
215
    # Build the Atom feed
    feed = boopfeed.Atom()
    feed.add_header(
        {
            "title": site_title,
            "self_link": f"{site_url}feeds/all.atom.xml",
            "site_link": site_url,
            "id": f"urn:uuid:{site_uuid}",
            "updated": current_datetime,
        }
216
217
218
219
    )

    for article in articles:
        # and complete it for each article
220
221
222
223
224
225
226
227
        entry = {
            "title": article.title(),
            "id": f"urn:uuid:{article.uuid()}",
            "article_link": article.url(),
            "published": article.date(),
            "content": article.content(),
        }

228
229
        article_author = article.author()
        if article_author:
230
            entry["author"] = article_author
231
232
        article_update = article.update()
        if article_update:
233
234
235
            entry["updated"] = article_update

        feed.add_entry(entry)
236
237

    # And write it on the filesystem
238
239
    feed_dirpath = os.path.join(output_path, "feeds")
    os.mkdir(feed_dirpath)
240
    feed_filepath = os.path.join(feed_dirpath, "all.atom.xml")
241
    feed.write(feed_filepath)
242

243
244

if __name__ == "__main__":
245
246
247
248
249
    try:
        main()
        print("Boop!")
    except ProgramError as e:
        print(e.args[0], file=sys.stderr)