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 | def add_sitemap_url(url): |
43 | url_obj = sitemap_root.createElement("url") |
44 | loc_obj = sitemap_root.createElement("loc") |
45 | loc_obj.appendChild(sitemap_root.createTextNode(url)) |
46 | url_obj.appendChild(loc_obj) |
47 | sitemap_urlset.appendChild(url_obj) |
48 |
|
49 |
|
50 | # Site template implementation; returns dict {filename: data} |
51 | def template() -> {str: str}: |
52 | files = {} |
53 |
|
54 | articles_list = [] |
55 | fg = FeedGenerator() |
56 | fg.id("https://joshstock.in/blog") |
57 | fg.title("Blog - Josh Stockin") |
58 | fg.author({"name": "Josh Stockin", "email": "josh@joshstock.in", "uri": "https://joshstock.in"}) |
59 | fg.link(href="https://joshstock.in/blog", rel="alternate") |
60 | fg.subtitle(blog_description) |
61 | fg.link(href="https://joshstock.in/atom", rel="self") |
62 | fg.language("en") |
63 |
|
64 | for content_file in list_files(CONTENT_DIRECTORY, ".md"): |
65 | f = open(content_file, "r") |
66 | data = f.read() |
67 | f.close() |
68 |
|
69 | content_html = markdown2.markdown( |
70 | data, |
71 | safe_mode=False, |
72 | extras=[ |
73 | "code-friendly", |
74 | "cuddled-lists", |
75 | "fenced-code-blocks", |
76 | "footnotes", |
77 | "header-ids", |
78 | "metadata", |
79 | "strike", |
80 | "tables", |
81 | "wiki-tables", |
82 | "tag-friendly", |
83 | "target-blank-links", |
84 | ], |
85 | ) |
86 |
|
87 | page_data = namespace(content_html.metadata) |
88 |
|
89 | page_data.link = page_data.link or "" |
90 |
|
91 | if page_data.type == "website": |
92 | page_generator = hg.HTML( |
93 | generate("head.head", page_data), |
94 | hg.BODY( |
95 | *generate("header", page_data), |
96 | hg.DIV( |
97 | hg.DIV(hg.mark_safe(content_html), _class="content-body"), |
98 | hg.DIV(_class="vfill"), |
99 | generate("footer"), |
100 | _class="content-container", |
101 | ), |
102 | onscroll="scroll()", |
103 | ), |
104 | ) |
105 | files[page_data.index] = hg.render(page_generator, {}).encode("utf-8") |
106 | if page_data.index != "index.html": |
107 | add_sitemap_url("/" + page_data.index.rsplit(".html")[0]) |
108 | else: |
109 | add_sitemap_url("/") |
110 |
|
111 | elif page_data.type == "article": # Blog article handling |
112 | page_data.readtime = readtime.of_html(content_html, wpm=150) |
113 | page_data.thumbnail = page_data.banner_image |
114 | page_data.link = "/blog/" + page_data.identifier |
115 | page_data.links = page_data.links or {} |
116 | articles_list += [generate("blog.listing", page_data)] |
117 | page_data.content = content_html |
118 |
|
119 | fe = fg.add_entry() |
120 | fe.id("https://joshstock.in/blog/" + page_data.identifier) |
121 | fe.author({"name": "Josh Stockin", "email": "josh@joshstock.in", "uri": "https://joshstock.in"}) |
122 | fe.title(page_data.title) |
123 | fe.summary(page_data.description + " / https://joshstock.in/blog/" + page_data.identifier) |
124 | datetime_pub = datetime.strptime(page_data.datestring, "%Y-%m-%d").replace(tzinfo=timezone(-timedelta(hours=6))) |
125 | fe.published(datetime_pub) |
126 | fe.updated(datetime_pub) |
127 | fe.link(href="https://joshstock.in/blog/" + page_data.identifier) |
128 |
|
129 | page_generator = hg.HTML( |
130 | generate("head.head", page_data), |
131 | hg.BODY( |
132 | *generate("header", page_data), |
133 | hg.DIV( |
134 | hg.DIV( |
135 | *generate("blog.article", page_data), _class="content-body" |
136 | ), |
137 | hg.DIV(_class="vfill"), |
138 | generate("footer"), |
139 | _class="content-container", |
140 | ), |
141 | onscroll="scroll()", |
142 | ), |
143 | ) |
144 |
|
145 | files["blog/" + page_data.identifier + ".html"] = hg.render( |
146 | page_generator, {} |
147 | ).encode("utf-8") |
148 |
|
149 | # Create blog index page |
150 | blog_page_data = namespace( |
151 | title="Blog", |
152 | banner_image="", |
153 | thumbnail="", |
154 | link="/blog", |
155 | description=fg.subtitle(), |
156 | ) |
157 | blog_page_generator = hg.HTML( |
158 | generate("head.head", blog_page_data), |
159 | hg.BODY( |
160 | *generate("header", blog_page_data), |
161 | hg.DIV( |
162 | hg.DIV( |
163 | hg.DIV( |
164 | hg.H1("Blog ", hg.IMG(src="/static/svg/memo.svg", _class="inline svg")), |
165 | hg.P( |
166 | fg.subtitle(), hg.BR(), |
167 | hg.SPAN("[", hg.A("Atom feed", href="/atom"), "] ", style="font-size: 0.75em; color: var(--caption-color)"), |
168 | hg.SPAN("[", hg.A("RSS feed", href="/rss"), "]", style="font-size: 0.75em; color: var(--caption-color)") |
169 | ) |
170 | ), |
171 | *articles_list, |
172 | _class="content-body", |
173 | ), |
174 | hg.DIV(_class="vfill"), |
175 | generate("footer"), |
176 | _class="content-container", |
177 | ), |
178 | onscroll="scroll()", |
179 | ), |
180 | ) |
181 | files["blog.html"] = hg.render(blog_page_generator, {}).encode("utf-8") |
182 | add_sitemap_url("/blog") |
183 |
|
184 | # Feeds |
185 | files["atom.xml"] = fg.atom_str(pretty=True) |
186 | fg.link(href="https://joshstock.in/rss", rel="self", replace=True) |
187 | files["rss.xml"] = fg.rss_str(pretty=True) |
188 |
|
189 | # Compile Sass stylesheets |
190 | for stylesheet_file in list_files(SASS_DIRECTORY, ".scss"): |
191 | if os.path.basename(stylesheet_file)[0] != "_": |
192 | files[ |
193 | os.path.join( |
194 | "static", |
195 | "style", |
196 | os.path.splitext(os.path.relpath(stylesheet_file, SASS_DIRECTORY))[ |
197 | 0 |
198 | ] |
199 | + ".css", |
200 | ) |
201 | ] = sass.compile(filename=stylesheet_file, output_style="compressed").encode("utf-8") |
202 |
|
203 | # Copy content from static files |
204 | for static_file in list_files(STATIC_DIRECTORY): |
205 | f = open(static_file, "rb") |
206 | data = f.read() |
207 | f.close() |
208 |
|
209 | files[ |
210 | os.path.join("static", os.path.relpath(static_file, STATIC_DIRECTORY)) |
211 | ] = data |
212 |
|
213 | files["sitemap.xml"] = sitemap_root.toprettyxml(indent="\t").encode("utf-8") |
214 |
|
215 | return files |
216 |
|