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=="project" |
154 | elif page_data.type == "project": |
155 | pass |
156 | add_sitemap_url("/projects/" + page_data.identifier, priority=0.6) |
157 | # type=="project" |
158 |
|
159 | # end of content md files |
160 |
|
161 | # Template strings |
162 | articles_list = sorted(articles_list, key=lambda x: x.datestring, reverse=True) |
163 | template_strings["articles_list"] = hg.render(hg.DIV(*[generate("blog.listing", x) for x in articles_list]), {}) |
164 | template_strings["projects_list"] = '<p>Under construction; check <a rel="noopener" target="_blank" href="https://git.joshstock.in">Git repositories</a> instead</p>' |
165 |
|
166 | # Apply templates |
167 | for website_page in website_pages: |
168 | content_html = website_page[0] |
169 | page_data = website_page[1] |
170 |
|
171 | templated = content_html.format_map(template_strings) |
172 | rendered = render_basic_page(page_data, templated) |
173 |
|
174 | files[page_data.index] = rendered |
175 | if page_data.index != "index.html": |
176 | add_sitemap_url("/" + page_data.index.rsplit(".html")[0], priority=0.8) |
177 | else: |
178 | add_sitemap_url("/", priority=1.0) |
179 |
|
180 | # Create article entries for feed generator |
181 | for page_data in articles_list: |
182 | fe = fg.add_entry() |
183 | fe.id("https://joshstock.in/blog/" + page_data.identifier) |
184 | fe.author({"name": "Josh Stockin", "email": "josh@joshstock.in", "uri": "https://joshstock.in"}) |
185 | fe.title(page_data.title) |
186 | fe.summary(page_data.description + " / https://joshstock.in/blog/" + page_data.identifier) |
187 | datetime_pub = datetime.strptime(page_data.datestring, "%Y-%m-%d").replace(tzinfo=timezone(-timedelta(hours=6))) |
188 | fe.published(datetime_pub) |
189 | fe.updated(datetime_pub) |
190 | fe.link(href="https://joshstock.in/blog/" + page_data.identifier) |
191 |
|
192 | # Generate Atom and RSS fees for blog |
193 | fg.link(href="https://joshstock.in/atom", rel="self") |
194 | files["atom.xml"] = fg.atom_str(pretty=True) |
195 | fg.link(href="https://joshstock.in/rss", rel="self", replace=True) |
196 | files["rss.xml"] = fg.rss_str(pretty=True) |
197 |
|
198 | # Compile Sass stylesheets |
199 | for stylesheet_file in list_files(SASS_DIRECTORY, ".scss"): |
200 | if os.path.basename(stylesheet_file)[0] != "_": |
201 | files[ |
202 | os.path.join( |
203 | "static", |
204 | "style", |
205 | os.path.splitext(os.path.relpath(stylesheet_file, SASS_DIRECTORY))[ |
206 | 0 |
207 | ] |
208 | + ".css", |
209 | ) |
210 | ] = sass.compile(filename=stylesheet_file, output_style="compressed").encode("utf-8") |
211 |
|
212 | # Copy content from static files |
213 | for static_file in list_files(STATIC_DIRECTORY): |
214 | f = open(static_file, "rb") |
215 | data = f.read() |
216 | f.close() |
217 |
|
218 | files[ |
219 | os.path.join("static", os.path.relpath(static_file, STATIC_DIRECTORY)) |
220 | ] = data |
221 |
|
222 | # Compile XML, export sitemap |
223 | files["sitemap.xml"] = sitemap_root.toprettyxml(indent="\t").encode("utf-8") |
224 |
|
225 | return files |
226 |
|