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