render.py
-rwxr-xr-x 7.9 KiB
1#!/usr/bin/env python
2import argparse
3import os
4import io
5import sys
6import xml.etree.ElementTree as ET
7from io import StringIO
8from datetime import datetime
9
10import markdown
11import yaml
12
13from bs4 import BeautifulSoup
14from jinja2 import Environment, FunctionLoader
15
16DEVMODE_WEBSOCKET_ADDR = "ws://localhost:8888/ws"
17
18
19def _set_dates(entries):
20 for entry in entries:
21 if "date" in entry:
22 entry["date"] = datetime.fromisoformat(entry["date"])
23 if "others" in entry:
24 return _set_dates(entry["others"])
25
26
27def _get_dates(entries):
28 dates = []
29 for entry in entries:
30 if "date" in entry:
31 dates.append(entry["date"])
32 if "others" in entry:
33 for other in _get_dates(entry["others"]):
34 dates.append(other)
35 return dates
36
37
38def _get_latest(entries):
39 latest = None
40 for other in _get_dates(entries):
41 if latest is None:
42 latest = other
43 else:
44 if other > latest:
45 latest = other
46 return latest
47
48
49def _load_sitemap(path, current_path):
50 by_name = dict()
51 flattened = []
52 with open(path, "r") as fp:
53 _sitemap = yaml.safe_load(fp.read())
54
55 if not _sitemap["enabled"]:
56 return None
57
58 variables = dict()
59 if "variables" in _sitemap:
60 variables = _sitemap["variables"]
61
62 def _filter_entries(entries):
63 entries = list(
64 filter(
65 lambda entry: "enabled" not in entry or entry["enabled"] is True,
66 entries,
67 )
68 )
69 for entry in entries:
70 if "others" in entry:
71 entry["others"] = _filter_entries(entry["others"])
72 return entries
73
74 entries = _filter_entries(_sitemap["entries"])
75
76 def _link(entries, parent=None):
77 for entry in entries:
78 flattened.append(entry)
79 entry["parent"] = parent
80 if "others" in entry:
81 _link(entry["others"], parent=entry)
82
83 def _make_url(link):
84 path = [link["name"]]
85 parent = link["parent"]
86 while parent is not None:
87 path.append(parent["name"])
88 parent = parent["parent"]
89 path.reverse()
90 return "/".join(path)
91
92 _link(entries, parent=None)
93
94 for entry in flattened:
95 target = "/" + _make_url(entry)
96 if target == current_path:
97 entry["active"] = True
98 else:
99 entry["active"] = False
100 entry["url"] = target
101 by_name[target] = entry
102
103 _set_dates(entries)
104 latest = _get_latest(entries)
105
106 return dict(by_name=by_name, variables=variables, links=entries, latest=_get_latest(entries))
107
108
109def _make_tree(links):
110 def _populate(root, links):
111 for link in links:
112 elm = ET.Element("li")
113 if "directory" in link and link["directory"]:
114 elm.text = link["name"]
115 else:
116 lref = ET.Element("a", href=link["url"])
117 link_text = link["name"]
118 if "date" in link:
119 link_text = link_text + " [" + str(link["date"].strftime("%Y-%m-%d")) + "]"
120 lref.text = link_text
121 if link["active"]:
122 lref.text = lref.text + " <"
123 elm.append(lref)
124 if "others" in link:
125 others = link["others"]
126 ul = ET.Element("ul")
127 elm.append(ul)
128 _populate(root=ul, links=others)
129 root.append(elm)
130
131 params = {"class": "tree"}
132
133 root = ET.Element("ul", **params)
134 _populate(root=root, links=links)
135 return ET.tostring(root).decode()
136
137
138def load_content(path):
139 with open(path) as fp:
140 text = fp.read()
141 content = markdown.markdown(
142 text,
143 extensions=["fenced_code", "tables"],
144 )
145 return content
146
147
148def generate(template, content_path, stylesheet, link, sitemap, devmode=False):
149 tree = _make_tree(sitemap["links"])
150 with open(template, "r") as fp:
151 template = fp.read()
152 env = Environment(loader=FunctionLoader(lambda name: template))
153 base = env.get_template(template)
154 with open(stylesheet) as fp:
155 style = fp.read()
156 content = load_content(content_path)
157 env_content = Environment(loader=FunctionLoader(lambda name: content))
158 out = base.render(
159 {
160 "content": env_content.get_template("").render(
161 {"sitemap": sitemap, "tree": tree, "variables": sitemap["variables"]}
162 ),
163 "variables": sitemap["variables"],
164 "is_index": False,
165 "link": link,
166 "style": style,
167 "sitemap": sitemap,
168 "tree": tree,
169 "devmode": devmode,
170 "devmode_websocket_addr": DEVMODE_WEBSOCKET_ADDR,
171 "current_time": datetime.now(),
172 }
173 )
174 sys.stdout.write(out)
175
176
177def generate_rss(sitemap):
178 sitemap = _load_sitemap(sitemap, "/")
179
180 feed = ET.Element("feed", attrib={"xmlns": "http://www.w3.org/2005/Atom"})
181
182 feed_id = ET.SubElement(feed, "id")
183 feed_id.text = "https://kevinschoon.com/"
184 title = ET.SubElement(feed, "title")
185 title.text = "Blog of Kevin Schoon"
186 link = ET.SubElement(feed, "link")
187 link.attrib["rel"] = "self"
188 link.attrib["href"] = "https://kevinschoon.com"
189 updated = ET.SubElement(feed, "updated")
190 updated.text = sitemap["latest"].isoformat()
191 author = ET.SubElement(feed, "author")
192 name = ET.SubElement(author, "name")
193 name.text = "Kevin Schoon"
194
195 for entry in sitemap["by_name"]["/blog"]["others"]:
196 absolute_url = "https://kevinschoon.com/blog/" + entry["name"]
197 atom_entry = ET.SubElement(feed, "entry")
198 link = ET.SubElement(atom_entry, "link")
199 link.attrib["href"] = absolute_url
200 link.attrib["rel"] = "self"
201 _id = ET.SubElement(atom_entry, "id")
202 _id.text = absolute_url
203 title = ET.SubElement(atom_entry, "title")
204 title.text = entry["tagline"]
205 updated = ET.SubElement(atom_entry, "updated")
206 updated.text = entry["date"].isoformat()
207 content_path = "content/blog/" + entry["name"] + "/README.md"
208 content = load_content(content_path)
209 content_element = ET.SubElement(atom_entry, "content")
210 content_element.attrib["type"] = "xhtml"
211 content_element.attrib["xml:lang"] = "en"
212 content_tree = ET.parse(io.StringIO(f"<div>{content}</div>")).getroot()
213 content_tree.attrib["xmlns"] = "http://www.w3.org/1999/xhtml"
214 content_element.append(content_tree)
215
216
217 xml_str = ET.tostring(feed, xml_declaration=True, encoding="unicode", method="xml")
218 xml_str = xml_str.replace(
219 "<?xml version='1.0' encoding='utf-8'?>",
220 "<?xml version='1.0' encoding='utf-8'?><?xml-stylesheet href='/style.xsl' type='text/xsl'?>",
221 )
222 sys.stdout.buffer.write(bytes(xml_str, encoding="utf-8"))
223
224
225if __name__ == "__main__":
226 parser = argparse.ArgumentParser(description="static renderer")
227 parser.add_argument("-rss", help="if rendering the rss feed", action="store_true")
228 parser.add_argument("-content", help="markdown file with content")
229 parser.add_argument(
230 "-template", default="index.html.jinja", help="jinja template file"
231 )
232 parser.add_argument("-stylesheet", default="./assets/main.css", help="style sheet")
233 parser.add_argument("-sitemap", default="sitemap.yaml", help="sitemap")
234 parser.add_argument("-link", default="/", help="current url")
235 parser.add_argument(
236 "-devmode", action="store_true", help="developer mode (websocket page refresh)"
237 )
238 args = parser.parse_args()
239 sitemap = _load_sitemap(args.sitemap, args.link)
240 if args.rss:
241 generate_rss(args.sitemap)
242 else:
243 generate(
244 template=args.template,
245 content_path=args.content,
246 stylesheet=args.stylesheet,
247 link=args.link,
248 sitemap=sitemap,
249 devmode=args.devmode,
250 )