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