Skip to content

Commit 9fd7a44

Browse files
committed
first site
1 parent 093438d commit 9fd7a44

File tree

7 files changed

+806
-0
lines changed

7 files changed

+806
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.pyd
2+
__pycache__
3+
.DS_Store
4+
output/

makesite.py

Lines changed: 388 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,388 @@
1+
"""
2+
Script to build a website from a bunch of markdown files.
3+
Inspired by https://github.com/sunainapai/makesite
4+
Tweaked for almarklein.org
5+
Then for pygfx.org
6+
"""
7+
8+
import os
9+
import shutil
10+
import webbrowser
11+
12+
import markdown
13+
import pygments
14+
from pygments.formatters import HtmlFormatter
15+
from pygments.lexers import get_lexer_by_name
16+
17+
18+
TITLE = "pygfx.org"
19+
20+
NAV = {
21+
"Main": "index",
22+
"Sponsor": "sponsor",
23+
# "Blog": "blog",
24+
# "Archive": "archive",
25+
# "Social": {
26+
# 'Twitter': 'https://twitter.com/pygfx',
27+
# },
28+
}
29+
30+
NEWS = {
31+
"Released pygfx v0.5.0": "https://github.com/pygfx/pygfx/releases/tag/v0.5.0",
32+
"Released wgpu-py v0.18.1": "https://github.com/pygfx/wgpu-py/releases/tag/v0.18.1",
33+
}
34+
35+
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
36+
OUT_DIR = os.path.join(THIS_DIR, "output")
37+
STATIC_DIR = os.path.join(THIS_DIR, "static")
38+
PAGES_DIR = os.path.join(THIS_DIR, "pages")
39+
POSTS_DIR = os.path.join(THIS_DIR, "posts")
40+
41+
42+
REDIRECT = '<html><head><meta HTTP-EQUIV="REFRESH" content="0; url=URL"></head></html>'
43+
44+
45+
def create_menu(page):
46+
""" Create the menu for the given page.
47+
"""
48+
menu = [""]
49+
50+
menu.append('<span class="header">Pages</span>')
51+
for title, target in NAV.items():
52+
if isinstance(target, str):
53+
if target.startswith(("https://", "http://", "/")):
54+
menu.append(f"<a href='{target}'>{title}</a>")
55+
else:
56+
menu.append(f"<a href='{target}.html'>{title}</a>")
57+
if target == page.name:
58+
menu[-1] = menu[-1].replace("<a ", '<a class="current" ')
59+
elif isinstance(target, dict):
60+
menu.append(f"<a href='{target.get('', '#')}.html'>{title}</a>")
61+
if target.get("", None) == page.name:
62+
menu[-1] = menu[-1].replace("<a ", '<a class="current" ')
63+
if True: # any(page.name == subtarget for subtarget in target.values()):
64+
for subtitle, subtarget in target.items():
65+
if not subtitle:
66+
continue
67+
if subtarget.startswith(("https://", "http://", "/")):
68+
menu.append(f"<a class='sub' href='{subtarget}'>{subtitle}</a>")
69+
else:
70+
menu.append(
71+
f"<a class='sub' href='{subtarget}.html'>{subtitle}</a>"
72+
)
73+
if subtarget == page.name:
74+
menu[-1] = menu[-1].replace("class='", "class='current ")
75+
else:
76+
raise RuntimeError(f"Unexpected NAV entry {type(target)}")
77+
78+
subtitles = [title for level, title in page.headers if level == 2]
79+
if subtitles:
80+
menu.append("<br /><span class='header'>Current page</span>")
81+
menu += [
82+
f"<a class='sub' href='#{title.lower()}'>{title}</a>" for title in subtitles
83+
]
84+
85+
if NEWS:
86+
menu.append('<br /><span class="header">News</span>')
87+
for title, url in NEWS.items():
88+
# menu.append(f"<a class='sub' href='{url}'>{title}</a>")
89+
menu.append(f"<a href='{url}'>{title}</a>")
90+
91+
return "<br />".join(menu)
92+
93+
94+
def create_blog_relatated_pages(posts):
95+
""" Create blog overview page.
96+
"""
97+
98+
# Filter and sort
99+
posts = [
100+
post for post in posts.values() if post.date and not post.name.startswith("_")
101+
]
102+
posts.sort(key=lambda p: p.date)
103+
104+
blogpages = {}
105+
106+
# Generate overview page
107+
html = ["<h1>Blog</h1>"]
108+
for page in reversed(posts):
109+
text = page.md
110+
if "<!-- END_SUMMARY -->" in text:
111+
summary = text.split("<!-- START_SUMMARY -->")[-1].split(
112+
"<!-- END_SUMMARY -->"
113+
)[0]
114+
else:
115+
summary = text.split("## ")[0]
116+
summary = summary.split("-->")[-1]
117+
118+
# html.append("<hr />" + page.date_and_tags_html)
119+
html.append("<div style='border-top: 1px solid #ddd;'>" + page.date_and_tags_html + "</div>")
120+
html.append(f'<a class="header" href="{page.name}.html"><h3>{page.title}</h3></a>')
121+
if page.thumbnail:
122+
html.append(f"<a href='{page.name}.html'><img src='{page.thumbnail}' class='thumb' /></a>")
123+
# html.append(f'<h2>{page.title}</h2>')
124+
html.append("<p>" + summary + "</p>")
125+
html.append(f"<a href='{page.name}.html'>read more ...</a><br /><br />")
126+
html.append("<div style='clear: both;'></div>")
127+
blogpages["overview"] = "\n".join(html)
128+
129+
# Generate archive page
130+
year = ""
131+
html = ["<h1>Archive</h1>\n"]
132+
for page in reversed(posts):
133+
if page.date[:4] != year:
134+
year = page.date[:4]
135+
html.append(f"<h2>{year}</h2>")
136+
html.append(f'{page.date}: <a href="{page.name}.html">{page.title}</a><br />')
137+
blogpages["archive"] = "\n".join(html)
138+
139+
# todo: Generate page for each tag
140+
141+
return blogpages
142+
143+
144+
def create_assets():
145+
""" Returns a dict of all the assets representing the website.
146+
"""
147+
assets = {}
148+
149+
# Load all static files
150+
for root, dirs, files in os.walk(STATIC_DIR):
151+
for fname in files:
152+
filename = os.path.join(root, fname)
153+
with open(filename, "rb") as f:
154+
assets[os.path.relpath(filename, STATIC_DIR)] = f.read()
155+
156+
# Collect pages
157+
pages = {}
158+
for fname in os.listdir(PAGES_DIR):
159+
if fname.lower().endswith(".md"):
160+
name = fname.split(".")[0].lower()
161+
with open(os.path.join(PAGES_DIR, fname), "rb") as f:
162+
md = f.read().decode()
163+
pages[name] = Page(name, md)
164+
165+
# Collect blog posts
166+
posts = {}
167+
for fname in os.listdir(POSTS_DIR):
168+
if fname.lower().endswith(".md"):
169+
name = fname.split(".")[0].lower()
170+
assert name not in pages, f"blog post slug not allowed: {name}"
171+
with open(os.path.join(POSTS_DIR, fname), "rb") as f:
172+
md = f.read().decode()
173+
posts[name] = Page(name, md)
174+
175+
# Get template
176+
with open(os.path.join(THIS_DIR, "template.html"), "rb") as f:
177+
html_template = f.read().decode()
178+
179+
with open(os.path.join(THIS_DIR, "style.css"), "rb") as f:
180+
css = f.read().decode()
181+
css += "/* Pygments CSS */\n" + HtmlFormatter(style="vs").get_style_defs(
182+
".highlight"
183+
)
184+
185+
# Generate posts
186+
for page in posts.values():
187+
page.prepare(pages.keys())
188+
title = page.title
189+
menu = create_menu(page)
190+
html = html_template.format(
191+
title=title, style=css, body=page.to_html(), menu=menu
192+
)
193+
print("generating post", page.name + ".html")
194+
assets[page.name + ".html"] = html.encode()
195+
196+
# Generate pages
197+
for page in pages.values():
198+
page.prepare(pages.keys())
199+
title = TITLE if page.name == "index" else TITLE + " - " + page.title
200+
menu = create_menu(page)
201+
html = html_template.format(
202+
title=title, style=css, body=page.to_html(), menu=menu
203+
)
204+
print("generating page", page.name + ".html")
205+
assets[page.name + ".html"] = html.encode()
206+
207+
# Generate special pages
208+
fake_md = "" # "##index\n## archive\n## tags"
209+
for name, html in create_blog_relatated_pages(posts).items():
210+
name = "blog" if name == "overview" else name
211+
print("generating page", name + ".html")
212+
assets[f"{name}.html"] = html_template.format(
213+
title=TITLE, style=css, body=html, menu=create_menu(Page("", fake_md))
214+
).encode()
215+
216+
# Backwards compat with previous site
217+
for page in pages.values():
218+
assets["pages/" + page.name + ".html"] = REDIRECT.replace(
219+
"URL", f"/{page.name}.html"
220+
).encode()
221+
222+
# Fix backslashes on Windows
223+
for key in list(assets.keys()):
224+
if "\\" in key:
225+
assets[key.replace("\\", "/")] = assets.pop(key)
226+
227+
return assets
228+
229+
230+
def main():
231+
""" Main function that exports the page to the file system.
232+
"""
233+
# Create / clean output dir
234+
if os.path.isdir(OUT_DIR):
235+
shutil.rmtree(OUT_DIR)
236+
os.mkdir(OUT_DIR)
237+
238+
# Write all assets to the directory
239+
for fname, bb in create_assets().items():
240+
filename = os.path.join(OUT_DIR, fname)
241+
dirname = os.path.dirname(filename)
242+
if not os.path.isdir(dirname):
243+
os.makedirs(dirname)
244+
with open(filename, "wb") as f:
245+
f.write(bb)
246+
247+
248+
class Page:
249+
""" Representation of a page. It takes in markdown and produces HTML.
250+
"""
251+
252+
def __init__(self, name, markdown):
253+
self.name = name
254+
self.md = markdown
255+
self.parts = []
256+
self.headers = []
257+
258+
self.title = name
259+
if markdown.startswith("# "):
260+
self.title = markdown.split("\n")[0][1:].strip()
261+
262+
self.date = None
263+
if "<!-- DATE:" in markdown:
264+
self.date = markdown.split("<!-- DATE:")[-1].split("-->")[0].strip() or None
265+
if self.date is not None:
266+
assert (
267+
len(self.date) == 10 and self.date.count("-") == 2
268+
), f"Weird date in {name}.md"
269+
270+
self.author = None
271+
if "<!-- AURTHOR:" in markdown:
272+
self.author = markdown.split("<!-- AURTHOR:")[-1].split("-->")[0].strip()
273+
274+
self.tags = []
275+
if "<!-- TAGS:" in markdown:
276+
self.tags = [
277+
x.strip()
278+
for x in markdown.split("<!-- TAGS:")[-1].split("-->")[0].split(",")
279+
]
280+
281+
self.date_and_tags_html = ""
282+
if self.date:
283+
self.date_and_tags_html = f"<span class='post-date-tags'>{', '.join(self.tags)}&nbsp;&nbsp;-&nbsp;&nbsp;{self.date}</span>"
284+
285+
self.thumbnail = None
286+
for fname in ["thumbs/" + self.name + ".jpg"]:
287+
if os.path.isfile(os.path.join(THIS_DIR, "static", fname)):
288+
self.thumbnail = fname
289+
290+
def prepare(self, page_names):
291+
# Convert markdown to HTML
292+
self.md = self._fix_links(self.md, page_names)
293+
self.md = self._highlight(self.md)
294+
self._split() # populates self.parts and self.headers
295+
296+
def _fix_links(self, text, page_names):
297+
""" Fix the markdown links based on the pages that we know.
298+
"""
299+
for n in page_names:
300+
text = text.replace(f"]({n})", f"]({n}.html)")
301+
text = text.replace(f"]({n}.md)", f"]({n}.html)")
302+
return text
303+
304+
def _highlight(self, text):
305+
""" Apply syntax highlighting.
306+
"""
307+
lines = []
308+
code = []
309+
for i, line in enumerate(text.splitlines()):
310+
if line.startswith("```"):
311+
if code:
312+
formatter = HtmlFormatter()
313+
try:
314+
lexer = get_lexer_by_name(code[0])
315+
except Exception:
316+
lexer = get_lexer_by_name("text")
317+
lines.append(
318+
pygments.highlight("\n".join(code[1:]), lexer, formatter)
319+
)
320+
code = []
321+
else:
322+
code.append(line[3:].strip()) # language
323+
elif code:
324+
code.append(line)
325+
else:
326+
lines.append(line)
327+
return "\n".join(lines).strip()
328+
329+
def _split(self):
330+
""" Split the markdown into parts based on sections.
331+
Each part is either text or a tuple representing a section.
332+
"""
333+
text = self.md
334+
self.parts = parts = []
335+
self.headers = headers = []
336+
lines = []
337+
338+
# Split in parts
339+
for line in text.splitlines():
340+
if line.startswith(("# ", "## ", "### ", "#### ", "##### ")):
341+
# Finish pending lines
342+
parts.append("\n".join(lines))
343+
lines = []
344+
# Process header
345+
level = len(line.split(" ")[0])
346+
title = line.split(" ", 1)[1]
347+
title_short = title.split("(")[0].split("<")[0].strip().replace("`", "")
348+
headers.append((level, title_short))
349+
parts.append((level, title_short, title))
350+
else:
351+
lines.append(line)
352+
parts.append("\n".join(lines))
353+
354+
# Now convert all text to html
355+
for i in range(len(parts)):
356+
if not isinstance(parts[i], tuple):
357+
parts[i] = markdown.markdown(parts[i], extensions=[]) + "\n\n"
358+
359+
def to_html(self):
360+
htmlparts = []
361+
for part in self.parts:
362+
if isinstance(part, tuple):
363+
level, title_short, title = part
364+
title_html = (
365+
title.replace("``", "`")
366+
.replace("`", "<code>", 1)
367+
.replace("`", "</code>", 1)
368+
)
369+
ts = title_short.lower()
370+
if part[0] == 1:
371+
htmlparts.append(self.date_and_tags_html)
372+
htmlparts.append("<h1>%s</h1>" % title_html)
373+
elif part[0] == 2 and title_short:
374+
htmlparts.append(
375+
"<a class='anch' name='{}' href='#{}'>".format(ts, ts)
376+
)
377+
htmlparts.append("<h%i>%s</h%i>" % (level, title_html, level))
378+
htmlparts.append("</a>")
379+
else:
380+
htmlparts.append("<h%i>%s</h%i>" % (level, title_html, level))
381+
else:
382+
htmlparts.append(part)
383+
return "\n".join(htmlparts)
384+
385+
386+
if __name__ == "__main__":
387+
main()
388+
# webbrowser.open(os.path.join(OUT_DIR, "index.html"))

0 commit comments

Comments
 (0)