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 += [generate("blog.listing", page_data)] |
119 | page_data.content = content_html |
120 |
|
121 | fe = fg.add_entry() |
122 | fe.id("https://joshstock.in/blog/" + page_data.identifier) |
123 | fe.author({"name": "Josh Stockin", "email": "josh@joshstock.in", "uri": "https://joshstock.in"}) |
124 | fe.title(page_data.title) |
125 | fe.summary(page_data.description + " / https://joshstock.in/blog/" + page_data.identifier) |
126 | datetime_pub = datetime.strptime(page_data.datestring, "%Y-%m-%d").replace(tzinfo=timezone(-timedelta(hours=6))) |
127 | fe.published(datetime_pub) |
128 | fe.updated(datetime_pub) |
129 | fe.link(href="https://joshstock.in/blog/" + page_data.identifier) |
130 |
|
131 | page_generator = hg.HTML( |
132 | generate("head.head", page_data), |
133 | hg.BODY( |
134 | *generate("header", page_data), |
135 | hg.DIV( |
136 | hg.DIV( |
137 | *generate("blog.article", page_data), _class="content-body" |
138 | ), |
139 | hg.DIV(_class="vfill"), |
140 | site_footer, |
141 | _class="content-container", |
142 | ), |
143 | onscroll="scroll()", |
144 | ), |
145 | ) |
146 |
|
147 | files["blog/" + page_data.identifier + ".html"] = hg.render( |
148 | page_generator, {} |
149 | ).encode("utf-8") |
150 |
|
151 | # Create blog index page |
152 | blog_page_data = namespace( |
153 | title="Blog", |
154 | banner_image="", |
155 | thumbnail="", |
156 | link="/blog", |
157 | description=fg.subtitle(), |
158 | ) |
159 | blog_page_generator = hg.HTML( |
160 | generate("head.head", blog_page_data), |
161 | hg.BODY( |
162 | *generate("header", blog_page_data), |
163 | hg.DIV( |
164 | hg.DIV( |
165 | hg.DIV( |
166 | hg.H1("Blog ", hg.IMG(src="/static/svg/memo.svg", _class="inline svg")), |
167 | hg.P( |
168 | fg.subtitle(), hg.BR(), |
169 | hg.SPAN("[", hg.A("Atom feed", href="/atom"), "] ", style="font-size: 0.75em; color: var(--caption-color)"), |
170 | hg.SPAN("[", hg.A("RSS feed", href="/rss"), "]", style="font-size: 0.75em; color: var(--caption-color)") |
171 | ) |
172 | ), |
173 | *articles_list, |
174 | _class="content-body", |
175 | ), |
176 | hg.DIV(_class="vfill"), |
177 | site_footer, |
178 | _class="content-container", |
179 | ), |
180 | onscroll="scroll()", |
181 | ), |
182 | ) |
183 | files["blog.html"] = hg.render(blog_page_generator, {}).encode("utf-8") |
184 | add_sitemap_url("/blog") |
185 |
|
186 | # Feeds |
187 | files["atom.xml"] = fg.atom_str(pretty=True) |
188 | fg.link(href="https://joshstock.in/rss", rel="self", replace=True) |
189 | files["rss.xml"] = fg.rss_str(pretty=True) |
190 |
|
191 | # Compile Sass stylesheets |
192 | for stylesheet_file in list_files(SASS_DIRECTORY, ".scss"): |
193 | if os.path.basename(stylesheet_file)[0] != "_": |
194 | files[ |
195 | os.path.join( |
196 | "static", |
197 | "style", |
198 | os.path.splitext(os.path.relpath(stylesheet_file, SASS_DIRECTORY))[ |
199 | 0 |
200 | ] |
201 | + ".css", |
202 | ) |
203 | ] = sass.compile(filename=stylesheet_file, output_style="compressed").encode("utf-8") |
204 |
|
205 | # Copy content from static files |
206 | for static_file in list_files(STATIC_DIRECTORY): |
207 | f = open(static_file, "rb") |
208 | data = f.read() |
209 | f.close() |
210 |
|
211 | files[ |
212 | os.path.join("static", os.path.relpath(static_file, STATIC_DIRECTORY)) |
213 | ] = data |
214 |
|
215 | files["sitemap.xml"] = sitemap_root.toprettyxml(indent="\t").encode("utf-8") |
216 |
|
217 | return files |
218 |
|