-
Notifications
You must be signed in to change notification settings - Fork 11
/
generator.py
144 lines (114 loc) · 4.44 KB
/
generator.py
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import locale
import os
from dataclasses import dataclass
from datetime import datetime
from html import escape
from pathlib import Path
import frontmatter
import minicli
import mistune
from jinja2 import Environment as Env
from jinja2 import FileSystemLoader
from slugify import slugify
__version__ = "0.0.1"
HERE = Path().resolve()
BLOG = HERE / "blog"
# Utile pour le rendu des langues en français.
locale.setlocale(locale.LC_ALL, "fr_FR.UTF-8")
NORMALIZED_STRFTIME = "%Y-%m-%dT12:00:00+01:00"
TODAY = datetime.today()
# Pour que Jinja2 sache où trouver les fichiers de templates.
environment = Env(loader=FileSystemLoader(str(HERE / "templates")))
# Le convertisseur markdown avec l’activation des plugins utiles.
md = mistune.create_markdown(
plugins=["strikethrough", "table", "url", "task_lists"],
escape=False,
hard_wrap=True,
)
@dataclass
class BlogPost:
item: dict
markdown: str
html: str
file_path: str
def __post_init__(self):
# Méta-données obligatoires.
self.titre = self.item["titre"]
self.titre_escape = escape(self.titre)
self.date_publication = self.item["date_publication"]
self.year = self.date_publication.year
self.slug = slugify(self.titre)
# Construction de l’URL unique pour cet article.
self.url = f"/blog/{self.year}/{self.slug}/"
self.full_url = f"http://scopyleft.fr{self.url}"
self.normalized_date = self.date_publication.strftime(NORMALIZED_STRFTIME)
self.escaped_content = escape(self.html)
def __eq__(self, other):
return self.url == other.url
def __lt__(self, other: "BlogPost"):
if not isinstance(other, BlogPost):
return NotImplemented
return self.date_publication < other.date_publication
@staticmethod
def all(source: Path):
items = []
for folder in each_folder_from(source):
for subfolder in each_folder_from(folder):
for markdown_file in each_file_from(subfolder, pattern="*.md"):
item = frontmatter.load(markdown_file)
html = md(item.content)
page = BlogPost(
item=item,
markdown=item.content,
html=html,
file_path=markdown_file.name,
)
items.append(page)
return sorted(items, reverse=True)
@minicli.cli
def build():
template_blogpost = environment.get_template("blogpost.html")
template_blogposts = environment.get_template("blogposts.html")
# On récupère toutes les blogposts.
blogposts = BlogPost.all(source=BLOG)
# Page d’accueil avec les derniers items.
content = template_blogposts.render(blogposts=blogposts)
Path(BLOG / "index.html").write_text(content)
# Page de détail d’un blogpost.
for blogpost in blogposts:
content = template_blogpost.render(blogpost=blogpost)
target_path = Path(BLOG / str(blogpost.year) / blogpost.slug)
target_path.mkdir(parents=True, exist_ok=True)
Path(target_path / "index.html").write_text(content)
@minicli.cli
def feed():
"""Generate a feed from last published items."""
template = environment.get_template("feed.xml")
# On récupère toutes les blogposts.
blogposts = BlogPost.all(source=BLOG)
content = template.render(
blogposts=blogposts,
current_dt=TODAY.strftime(NORMALIZED_STRFTIME),
BASE_URL="http://scopyleft.fr",
)
(HERE / "syndication" / "flux.atom").write_text(content)
# Des utilitaires pour parcourir les fichiers markdown des sujets.
def each_file_from(source_dir, pattern="*", exclude=None):
"""Walk across the `source_dir` and return the `pattern` file paths."""
for path in _each_path_from(source_dir, pattern=pattern, exclude=exclude):
if path.is_file():
yield path
def _each_path_from(source_dir, pattern="*", exclude=None):
for path in sorted(Path(source_dir).glob(pattern)):
if exclude is not None and path.name in exclude:
continue
yield path
def each_folder_from(source_dir, exclude=None):
"""Walk across the `source_dir` and return the folder paths."""
for direntry in os.scandir(source_dir):
if direntry.is_dir():
if exclude is not None and direntry.name in exclude:
continue
yield direntry
if __name__ == "__main__":
minicli.run()