1 | # targets.py / Template engine for my website |
2 | # Joshua Stockin / josh@joshstock.in / https://joshstock.in |
3 |
|
4 | import os |
5 | import html |
6 | from datetime import datetime, timezone, timedelta |
7 | from xml.dom import minidom |
8 |
|
9 | import markdown2 |
10 | import htmlgenerator as hg |
11 | import readtime |
12 | import sass |
13 | from feedgen.feed import FeedGenerator |
14 |
|
15 | from _utils import dotdict as namespace, current_dir, load_generators, list_files |
16 |
|
17 | # Site generation metadata |
18 | CONTENT_DIRECTORY = os.path.join(current_dir(), "content") |
19 | SASS_DIRECTORY = os.path.join(current_dir(), "style") |
20 | STATIC_DIRECTORY = os.path.join(current_dir(), "static") |
21 |
|
22 | blog_description = "Barely coherent ramblings about engineering projects, software, hardware, and other things." |
23 |
|
24 | # Fetch generator functions |
25 | GENERATORS_MODULE = "generators" |
26 | GENERATORS = [ |
27 | "head.head", |
28 | "header", |
29 | "footer", |
30 | "blog.article", |
31 | "blog.index", |
32 | "blog.listing", |
33 | ] |
34 |
|
35 | generate = load_generators(GENERATORS_MODULE, GENERATORS) |
36 |
|
37 | sitemap_root = minidom.Document() |
38 | sitemap_urlset = sitemap_root.createElementNS("http://www.sitemap.org/schemas/sitemap/0.9", "urlset") |
39 | sitemap_urlset.setAttribute("xmlns", sitemap_urlset.namespaceURI) |
40 | sitemap_root.appendChild(sitemap_urlset) |
41 |
|
42 | site_footer = generate("footer") |
43 |
|
44 | def add_sitemap_url(url): |
45 | url_obj = sitemap_root.createElement("url") |
46 | loc_obj = sitemap_root.createElement("loc") |
47 | loc_obj.appendChild(sitemap_root.createTextNode(url)) |
48 | url_obj.appendChild(loc_obj) |
49 | sitemap_urlset.appendChild(url_obj) |
50 |
|
51 |
|
52 | # Site template implementation; returns dict {filename: data} |
53 | def template() -> {str: str}: |
54 | files = {} |
55 |
|
56 | articles_list = [] |
57 | fg = FeedGenerator() |
58 | fg.id("https://joshstock.in/blog") |
59 | fg.title("Blog - Josh Stockin") |
60 | fg.author({"name": "Josh Stockin", "email": "josh@joshstock.in", "uri": "https://joshstock.in"}) |
61 | fg.link(href="https://joshstock.in/blog", rel="alternate") |
62 | fg.subtitle(blog_description) |
63 | fg.link(href="https://joshstock.in/atom", rel="self") |
64 | fg.language("en") |
65 |
|
66 | for content_file in list_files(CONTENT_DIRECTORY, ".md"): |
67 | f = open(content_file, "r") |
68 | data = f.read() |
69 | f.close() |
70 |
|
71 | content_html = markdown2.markdown( |
72 | data, |
73 | safe_mode=False, |
74 | extras=[ |
75 | "code-friendly", |
76 | "cuddled-lists", |
77 | "fenced-code-blocks", |
78 | "footnotes", |
79 | "header-ids", |
80 | "metadata", |
81 | "strike", |
82 | "tables", |
83 | "wiki-tables", |
84 | "tag-friendly", |
85 | "target-blank-links", |
86 | ], |
87 | ) |
88 |
|
89 | page_data = namespace(content_html.metadata) |
90 |
|
91 | page_data.link = page_data.link or "" |
92 |
|
93 | if page_data.type == "website": |
94 | page_generator = hg.HTML( |
95 | generate("head.head", page_data), |
96 | hg.BODY( |
97 | *generate("header", page_data), |
98 | hg.DIV( |
99 | hg.DIV(hg.mark_safe(content_html), _class="content-body"), |
100 | hg.DIV(_class="vfill"), |
101 | site_footer, |
102 | _class="content-container", |
103 | ), |
104 | onscroll="scroll()", |
105 | ), |
106 | ) |
107 | files[page_data.index] = hg.render(page_generator, {}).encode("utf-8") |
108 | if page_data.index != "index.html": |
109 | add_sitemap_url("/" + page_data.index.rsplit(".html")[0]) |
110 | else: |
111 | add_sitemap_url("/") |
112 |
|
113 | elif page_data.type == "article": # Blog article handling |
114 | page_data.readtime = readtime.of_html(content_html, wpm=150) |
115 | page_data.thumbnail = page_data.banner_image |
116 | page_data.link = "/blog/" + page_data.identifier |
117 | page_data.links = page_data.links or {} |
118 | articles_list += [page_data] |
119 | page_data.content = content_html |
120 |
|
121 | page_generator = hg.HTML( |
122 | generate("head.head", page_data), |
123 | hg.BODY( |
124 | *generate("header", page_data), |
125 | hg.DIV( |
126 | hg.DIV( |
127 | *generate("blog.article", page_data), _class="content-body" |
128 | ), |
129 | hg.DIV(_class="vfill"), |
130 | site_footer, |
131 | _class="content-container", |
132 | ), |
133 | onscroll="scroll()", |
134 | ), |
135 | ) |
136 |
|
137 | files["blog/" + page_data.identifier + ".html"] = hg.render( |
138 | page_generator, {} |
139 | ).encode("utf-8") |
140 |
|
141 |
|
142 | # Sort articles list by descending datestring |
143 | articles_list = sorted(articles_list, key=lambda x: x.datestring, reverse=True) |
144 |
|
145 | # Create article entries for feed generator |
146 | for page_data in articles_list: |
147 | fe = fg.add_entry() |
148 | fe.id("https://joshstock.in/blog/" + page_data.identifier) |
149 | fe.author({"name": "Josh Stockin", "email": "josh@joshstock.in", "uri": "https://joshstock.in"}) |
150 | fe.title(page_data.title) |
151 | fe.summary(page_data.description + " / https://joshstock.in/blog/" + page_data.identifier) |
152 | datetime_pub = datetime.strptime(page_data.datestring, "%Y-%m-%d").replace(tzinfo=timezone(-timedelta(hours=6))) |
153 | fe.published(datetime_pub) |
154 | fe.updated(datetime_pub) |
155 | fe.link(href="https://joshstock.in/blog/" + page_data.identifier) |
156 |
|
157 | # Create blog index page |
158 | blog_page_data = namespace( |
159 | title="Blog", |
160 | banner_image="", |
161 | thumbnail="", |
162 | link="/blog", |
163 | description=fg.subtitle(), |
164 | ) |
165 | blog_page_generator = hg.HTML( |
166 | generate("head.head", blog_page_data), |
167 | hg.BODY( |
168 | *generate("header", blog_page_data), |
169 | hg.DIV( |
170 | hg.DIV( |
171 | hg.DIV( |
172 | hg.H1("Blog ", hg.IMG(src="/static/svg/memo.svg", _class="inline svg")), |
173 | hg.P( |
174 | fg.subtitle(), hg.BR(), |
175 | hg.SPAN("[", hg.A("Atom feed", href="/atom"), "] ", style="font-size: 0.75em; color: var(--caption-color)"), |
176 | hg.SPAN("[", hg.A("RSS feed", href="/rss"), "]", style="font-size: 0.75em; color: var(--caption-color)") |
177 | ) |
178 | ), |
179 | *[generate("blog.listing", x) for x in articles_list], |
180 | _class="content-body", |
181 | ), |
182 | hg.DIV(_class="vfill"), |
183 | site_footer, |
184 | _class="content-container", |
185 | ), |
186 | onscroll="scroll()", |
187 | ), |
188 | ) |
189 | files["blog.html"] = hg.render(blog_page_generator, {}).encode("utf-8") |
190 | add_sitemap_url("/blog") |
191 |
|
192 | # Feeds |
193 | files["atom.xml"] = fg.atom_str(pretty=True) |
194 | fg.link(href="https://joshstock.in/rss", rel="self", replace=True) |
195 | files["rss.xml"] = fg.rss_str(pretty=True) |
196 |
|
197 | # Compile Sass stylesheets |
198 | for stylesheet_file in list_files(SASS_DIRECTORY, ".scss"): |
199 | if os.path.basename(stylesheet_file)[0] != "_": |
200 | files[ |
201 | os.path.join( |
202 | "static", |
203 | "style", |
204 | os.path.splitext(os.path.relpath(stylesheet_file, SASS_DIRECTORY))[ |
205 | 0 |
206 | ] |
207 | + ".css", |
208 | ) |
209 | ] = sass.compile(filename=stylesheet_file, output_style="compressed").encode("utf-8") |
210 |
|
211 | # Copy content from static files |
212 | for static_file in list_files(STATIC_DIRECTORY): |
213 | f = open(static_file, "rb") |
214 | data = f.read() |
215 | f.close() |
216 |
|
217 | files[ |
218 | os.path.join("static", os.path.relpath(static_file, STATIC_DIRECTORY)) |
219 | ] = data |
220 |
|
221 | files["sitemap.xml"] = sitemap_root.toprettyxml(indent="\t").encode("utf-8") |
222 |
|
223 | return files |
224 |
|