You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

851 lines
30 KiB

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""bute: Static site generator.
Usage:
Change the defautl values of base URLs and directory names in the __init__
of the class Website.
Example:
>>> python3 bute.py
Author:
Erik Johannes Husom
Created:
2021-01-27
"""
import datetime
from email import utils
import html
import os
import re
import markdown as md
class Website():
def __init__(self):
self.baseurl = "http://localhost:8000/"
self.layouts_folder = "layouts"
self.posts_folder = "posts"
self.pages_folder = "pages"
self.standalone_folder = "standalone"
self.photography_folder = "photography"
self.photofeed_folder = "photography/photofeed"
self.activities_folder = "activities"
self.posts_folder = "posts"
self.layout_filenames = ["head.html", "header.html", "footer.html"]
self.layout_files = []
self.img_exts = [".jpg", ".png", ".PNG", ".JPG", ".jpeg", ".JPEG"]
self.activity_exts = [".gpx"]
self.wide_pages = [
]
# Read the common layouts of each page
for f in self.layout_filenames:
with open(self.layouts_folder + "/" + f, "r") as infile:
self.layout_files.append(infile.read())
def combine_layouts(self, body):
page = "<!DOCTYPE html>\n"
page += '<html lang="en">\n'
page += self.layout_files[0]
page += " <body>\n"
page += self.layout_files[1]
page += body
page += self.layout_files[2]
page += " </body>\n"
page += "</html>"
return page
def save_page(self, page, name):
with open(name, "w") as outfile:
outfile.write(page)
print("Created", name)
def build_page(self, f):
# Check that the page is an HTML or Markdown file
if os.path.splitext(f)[1] not in [".html", ".md"]:
return 0
# Read the content for this specific page
with open(f, "r") as infile:
body = infile.read()
if os.path.splitext(f)[1] == ".md":
body = md.markdown(body, extensions=['fenced_code'])
f = f.replace(".md", ".html")
# Combine the common layouts and the page content
page = self.combine_layouts(body)
# Check if the page should have a wide body
if os.path.basename(f) in self.wide_pages:
page = page.replace("<body>", "<body class=wide>")
self.save_page(page, os.path.basename(f))
def find_pages(self, directory):
pages = []
for f in os.listdir(directory):
f = os.path.join(directory, f)
if os.path.isdir(f):
pgs = self.find_pages(f)
pages += pgs
pages.append(f)
return pages
def build_pages(self):
pages = self.find_pages(self.pages_folder)
# print(pages)
for p in pages:
self.build_page(p)
for f in os.listdir(self.standalone_folder):
# Check that the page is a .html file
if os.path.splitext(f)[1] != ".html":
continue
# Read the content for this specific page
with open(self.standalone_folder + "/" + f, "r") as infile:
body = infile.read()
self.save_page(body, f)
def build_blog(self, exclude_drafts=True):
blog_links = []
blog_titles = []
blog_dates = []
blog_rfcdates = []
blog_contents = []
blog_image_links = []
for f in os.listdir(self.posts_folder):
# if os.path.isdir(f) and "index.md" in os.listdir(f):
if os.path.isdir(self.posts_folder + "/" + f):
if "index.md" in os.listdir(self.posts_folder + "/" + f):
draft = False
with open(self.posts_folder + "/" + f + "/" + "index.md",
"r") as infile:
lines = infile.readlines()
title = ""
date = ""
for line in lines:
if line.startswith("title:"):
title = line.replace("title: ", "")
if line.startswith("date:"):
date = line.replace("date: ", "")
if line.startswith("draft:"):
draft = line.replace("draft: ", "")
draft = draft.replace('"', "")
draft = draft.strip()
if draft == "true" and exclude_drafts:
continue
title = title.replace('"', "")
date = datetime.datetime.strptime(date[:10], "%Y-%m-%d")
rfcdate = utils.format_datetime(date)
print_date = datetime.datetime.strftime(date, "%d %b %Y")
# os.system(
# "pandoc {}/{}/index.md -o {}/{}/index.html".format(
# self.posts_folder, f, self.posts_folder, f
# ))
with open(f"{self.posts_folder}/{f}/index.md", "r") as infile:
md_version = infile.read()
md_post = md_version.split("---")
md_front_matter = md_post[1]
md_content = " ".join(md_post[2:])
html_version = md.markdown(md_content, extensions=['fenced_code'])
with open(f"{self.posts_folder}/{f}/index.html", "w") as outfile:
outfile.write(html_version)
# body = "<h2>blog</h2>"
# body += "\n"
body = "<article>"
body += "\n"
body += "<h2>" + title + "</h2>"
body += "\n"
body += "<h3>" + print_date + "</h3>"
blog_link = self.posts_folder + "/" + f + "/" + "index.html"
with open(blog_link, "r") as infile:
content = infile.read()
content = content.replace("{{&lt; rawhtml &gt;}}", "")
content = content.replace("{{&lt; /rawhtml &gt;}}", "")
body += content
body += "</article>"
body += "\n"
page = self.combine_layouts(body)
self.save_page(page, blog_link)
date = datetime.datetime.strftime(date, "%Y-%m-%d")
blog_links.append(blog_link)
blog_titles.append(title)
blog_dates.append(date)
blog_rfcdates.append(rfcdate)
blog_contents.append(content)
blog_image_links.append(blog_link)
body = "<article>"
body += "<h2>Blog</h2>"
body += "\n"
body += "\n"
body += "<ul>"
body += "\n"
blog_dates, blog_titles, blog_links, blog_contents, blog_rfcdates, blog_image_links = zip(
*sorted(zip(blog_dates, blog_titles, blog_links, blog_contents,
blog_rfcdates, blog_image_links))
)
blog_dates = list(reversed(list(blog_dates)))
blog_links = list(reversed(list(blog_links)))
blog_titles = list(reversed(list(blog_titles)))
blog_contents = list(reversed(list(blog_contents)))
blog_rfcdates = list(reversed(list(blog_rfcdates)))
blog_image_links= list(reversed(list(blog_image_links)))
shortblogfeed = "<ul>"
length = 5
counter = 0
for l, t, d in zip(blog_links, blog_titles, blog_dates):
if counter < length:
shortblogfeed += f"<li><span class=date>{d}</span><a href='{l}'>{t}</a></li>"
counter += 1
# d = datetime.datetime.strftime(d, "%d %b %Y")
# d = datetime.datetime.strftime(d, "%Y-%m-%d")
body += f"<li><span class=date>{d}</span><a href='{l}'>{t}</a></li>"
body += "</ul>"
shortblogfeed += "</ul>"
page = self.combine_layouts(body)
self.save_page(page, "blog.html")
self.blog_dates = blog_dates
self.blog_links = blog_links
self.blog_titles = blog_titles
self.blog_contents = blog_contents
self.blog_rfcdates = blog_rfcdates
self.blog_image_links = blog_image_links
with open("index.html", "r") as f:
index = f.read()
index = index.replace("<!--BLOGFEED-->", shortblogfeed)
with open("index.html", "w") as f:
f.write(index)
def read_photo_feed(self, granularity="yearly"):
photofeed_links = []
photofeed_absolute_links = []
photofeed_titles = []
photofeed_dates = []
photofeed_months = []
photofeed_years = []
# Skip this function if the folder does not exists.
if not os.path.exists(self.photofeed_folder):
return 0
for img_name in os.listdir(self.photofeed_folder):
print(img_name)
if not os.path.splitext(img_name)[1].lower() in self.img_exts:
continue
date = datetime.datetime.strptime(img_name[:10], "%Y-%m-%d")
year_and_month = img_name[:7]
year = img_name[:4]
rfcdate = utils.format_datetime(date)
print_date = datetime.datetime.strftime(date, "%d %b %Y")
# Take the second part of filename (after the date) and
# split at the extension. Replace dashes with space.
title = img_name[11:].split(".")[0]
title = title.replace("-", " ")
if granularity == "monthly":
link = "photofeed-" + year_and_month + ".html#" + img_name[:10]
else:
link = "photofeed-" + year + ".html#" + img_name[:10]
image_link = self.photofeed_folder + "/" + img_name
content = f"<img src=\"{image_link}\" alt=''/><figcaption>{title}</figcaption>"
date = datetime.datetime.strftime(date, "%Y-%m-%d")
self.blog_links.append(link)
self.blog_titles.append(title)
self.blog_dates.append(date)
self.blog_rfcdates.append(rfcdate)
self.blog_contents.append(content)
self.blog_image_links.append(image_link)
photofeed_links.append(image_link)
photofeed_absolute_links.append(self.baseurl + link)
photofeed_titles.append(title)
photofeed_dates.append(date)
photofeed_months.append(year_and_month)
photofeed_years.append(year)
photofeed_dates, photofeed_links, photofeed_absolute_links, photofeed_titles, photofeed_months, photofeed_years = zip(
*sorted(zip(photofeed_dates, photofeed_links,
photofeed_absolute_links, photofeed_titles,
photofeed_months, photofeed_years))
)
photofeed_dates = list(reversed(list(photofeed_dates)))
photofeed_links = list(reversed(list(photofeed_links)))
photofeed_absolute_links = list(reversed(list(photofeed_absolute_links)))
photofeed_titles = list(reversed(list(photofeed_titles)))
photofeed_months = list(reversed(list(photofeed_months)))
photofeed_years = list(reversed(list(photofeed_years)))
self.blog_dates, self.blog_titles, self.blog_links, self.blog_contents, self.blog_rfcdates, self.blog_image_links = zip(
*sorted(zip(self.blog_dates, self.blog_titles, self.blog_links,
self.blog_contents, self.blog_rfcdates,
self.blog_image_links))
)
self.blog_dates = list(reversed(list(self.blog_dates)))
self.blog_links = list(reversed(list(self.blog_links)))
self.blog_titles = list(reversed(list(self.blog_titles)))
self.blog_contents = list(reversed(list(self.blog_contents)))
self.blog_rfcdates = list(reversed(list(self.blog_rfcdates)))
self.blog_image_links = list(reversed(list(self.blog_image_links)))
photofeed_pages = []
month_set = sorted(list(set(photofeed_months)))[::-1]
year_set = sorted(list(set(photofeed_years)))[::-1]
if granularity == "monthly":
period_set = month_set
photofeed_periods = photofeed_months
else:
period_set = year_set
photofeed_periods = photofeed_years
for i, period in enumerate(period_set):
body = "<article>"
body += f"<h2>Photofeed</h2>"
body += "\n"
# Add links to other years
body += "<ul>"
for period2 in period_set:
if period2 == period:
continue
body += f"<li><a href='photofeed-{period2}.html'>{period2}</a></li>"
body += "</ul>"
body += "<br/>"
body += "\n"
body += f"<h3>{period}</h3>"
body += "\n"
body += "\n"
body += "<section class=gallerymasonry>"
body += "\n"
for l, a, t, d, p in zip(photofeed_links, photofeed_absolute_links, photofeed_titles,
photofeed_dates, photofeed_periods):
# If image not in current period (month or year), skip it
if p != period:
continue
body += "<section class=galleryitem>"
body += "\n"
body += f"<a href=\"{l}\">"
body += f"<img id=\"{d}\" src=\"{l}\" title=\"{t}\"/>"
body += "</a>"
body += "\n"
body += f"<figcaption>{d}: {t} "
body += f"<a href=\"{a}\" class=\"shareButton\">(shareable link)</a>"
body += "</figcaption>"
body += "\n"
body += "</section>"
body += "\n"
body += "</section>"
body += "\n"
body += "</article>"
body += "\n"
page = self.combine_layouts(body)
self.save_page(page, f"photofeed-{period}.html")
if i == 0:
self.save_page(page, f"photofeed.html")
photofeed_pages.append([f"photofeed-{period}.html", period])
print(period)
def create_activity_feed(self):
# Skip this function if the folder does not exists.
if not os.path.exists(self.activities_folder):
return 0
activities_links = []
activities_absolute_links = []
activities_images = []
activities_titles = []
activities_texts = []
activities_dates = []
activities_months = []
activities_years = []
gpx_filename = None
for f in os.listdir(self.activities_folder):
# Each activity must have its own folder
if os.path.isdir(self.activities_folder + "/" + f):
activity_folder = self.activities_folder + "/" + f + "/"
gpx_filename = None
activity_text = None
activity_images = []
# Find gpx file in folder
for f2 in os.listdir(activity_folder):
if os.path.splitext(f2)[1].lower() in self.activity_exts:
gpx_filename = f2
# If there is a Markdown file in the folder, use the text
# as content.
if os.path.splitext(f2)[1].lower() == ".md":
with open(activity_folder + f2, "r") as infile:
activity_text = infile.read()
activity_text = md.markdown(activity_text)
if os.path.splitext(f2)[1].lower() in self.img_exts:
activity_images.append(activity_folder + f2)
# If no gpx file is found, continue to next filder
if gpx_filename == None:
continue
else:
continue
date = datetime.datetime.strptime(gpx_filename[:10], "%Y-%m-%d")
year_and_month = gpx_filename[:7]
year = gpx_filename[:4]
rfcdate = utils.format_datetime(date)
print_date = datetime.datetime.strftime(date, "%d %b %Y")
# Take the second part of filename (after the date) and
# split at the extension. Replace dashes with space.
title = os.path.splitext(gpx_filename[11:])[0]
title = title.replace("-", " ")
link = "activities-" + year + ".html#" + gpx_filename[:10]
gpx_link = activity_folder + gpx_filename
if activity_text == None:
content = title
else:
content = activity_text
date = datetime.datetime.strftime(date, "%Y-%m-%d")
self.blog_links.append(link)
self.blog_titles.append(title)
self.blog_dates.append(date)
self.blog_rfcdates.append(rfcdate)
self.blog_contents.append(content)
self.blog_image_links.append(gpx_link)
activities_links.append(gpx_link)
activities_absolute_links.append(self.baseurl + link)
activities_images.append(activity_images)
activities_titles.append(title)
activities_texts.append(activity_text)
activities_dates.append(date)
activities_months.append(year_and_month)
activities_years.append(year)
activities_dates, activities_links, activities_absolute_links, activities_images, activities_titles, activities_texts, activities_months, activities_years = zip(
*sorted(zip(activities_dates, activities_links,
activities_absolute_links, activities_images, activities_titles,
activities_texts,
activities_months, activities_years))
)
activities_dates = list(reversed(list(activities_dates)))
activities_links = list(reversed(list(activities_links)))
activities_absolute_links = list(reversed(list(activities_absolute_links)))
activities_images = list(reversed(list(activities_images)))
activities_titles = list(reversed(list(activities_titles)))
activities_texts = list(reversed(list(activities_texts)))
activities_months = list(reversed(list(activities_months)))
activities_years = list(reversed(list(activities_years)))
self.blog_dates, self.blog_titles, self.blog_links, self.blog_contents, self.blog_rfcdates, self.blog_image_links = zip(
*sorted(zip(self.blog_dates, self.blog_titles, self.blog_links,
self.blog_contents, self.blog_rfcdates,
self.blog_image_links))
)
self.blog_dates = list(reversed(list(self.blog_dates)))
self.blog_links = list(reversed(list(self.blog_links)))
self.blog_titles = list(reversed(list(self.blog_titles)))
self.blog_contents = list(reversed(list(self.blog_contents)))
self.blog_rfcdates = list(reversed(list(self.blog_rfcdates)))
self.blog_image_links = list(reversed(list(self.blog_image_links)))
activities_pages = []
month_set = sorted(list(set(activities_months)))[::-1]
year_set = sorted(list(set(activities_years)))[::-1]
period_set = year_set
activities_periods = activities_years
for i, period in enumerate(period_set):
body = "<article>"
body += """
<link rel="stylesheet" href="js/leaflet/leaflet.css" />
<link rel="stylesheet" href="js/leaflet-elevation/leaflet-elevation/leaflet-elevation.css" />
<script src="js/leaflet/leaflet.js"></script>
<script src="js/leaflet-elevation/leaflet-elevation/leaflet-elevation.js"></script>
<script src="js/gpx.js"></script>
"""
# <script src="js/leaflet-ui/leaflet-ui.js"></script>
body += """
<script>
function addText(text, divId) {
document.getElementById(divId).innerHTML = text;
}
// Full list options at "leaflet-elevation.js"
var elevation_options = {
// Default chart colors: theme lime-theme, magenta-theme, ...
theme: "lightblue-theme",
// Chart container outside/inside map container
detached: true,
// if (detached), the elevation chart container
elevationDiv: "#elevation-div",
// if (!detached) autohide chart profile on chart mouseleave
autohide: false,
// if (!detached) initial state of chart profile control
collapsed: false,
// if (!detached) control position on one of map corners
position: "topright",
// Toggle close icon visibility
closeBtn: false,
// Autoupdate map center on chart mouseover.
followMarker: true,
// Autoupdate map bounds on chart update.
autofitBounds: true,
// Chart distance/elevation units.
imperial: false,
// [Lat, Long] vs [Long, Lat] points. (leaflet default: [Lat, Long])
reverseCoords: false,
// Acceleration chart profile: true || "summary" || "disabled" || false
acceleration: false,
// Slope chart profile: true || "summary" || "disabled" || false
slope: false,
// Speed chart profile: true || "summary" || "disabled" || false
speed: true,
// Altitude chart profile: true || "summary" || "disabled" || false
altitude: true,
// Display time info: true || "summary" || false
time: true,
// Display distance info: true || "summary" || false
distance: true,
// Summary track info style: "inline" || "multiline" || false
summary: 'multiline',
// Download link: "link" || false || "modal"
downloadLink: false,
// Toggle chart ruler filter
ruler: true,
// Toggle chart legend filter
legend: true,
// Toggle "leaflet-almostover" integration
almostOver: true,
// Toggle "leaflet-distance-markers" integration
distanceMarkers: false,
// Toggle "leaflet-hotline" integration
hotline: true,
// Display track datetimes: true || false
timestamps: false,
// Display track waypoints: true || "markers" || "dots" || false
waypoints: true,
// Toggle custom waypoint icons: true || { associative array of <sym> tags } || false
wptIcons: {
'': L.divIcon({
className: 'elevation-waypoint-marker',
html: '<i class="elevation-waypoint-icon"></i>',
iconSize: [30, 30],
iconAnchor: [8, 30],
}),
},
// Toggle waypoint labels: true || "markers" || "dots" || false
wptLabels: true,
// Render chart profiles as Canvas or SVG Paths
preferCanvas: true,
};
</script>"""
body += f"<h2>Activities</h2>"
body += "\n"
# Add links to other years
body += "<ul>"
for period2 in period_set:
if period2 == period:
continue
body += f"<li><a href='activities-{period2}.html'>{period2}</a></li>"
body += "</ul>"
body += "<br/>"
body += "\n"
body += f"<h3>{period}</h3>"
body += "\n"
body += "\n"
body += "<section class=gallerymasonry>"
body += "\n"
count = 0
for l, a, im, t, e, d, p in zip(activities_links,
activities_absolute_links, activities_images, activities_titles,
activities_texts,
activities_dates, activities_periods):
print(l)
# If image not in current period (month or year), skip it
if p != period:
continue
# Make section in overview page
body += """<section class="galleryitem activity">"""
body += f"<h4>{d}: {t} "
body += f"<a href=\"{a}\" class=\"shareButton\">(shareable link)</a>"
body += "</h4>"
body += "\n"
body += f"""<div id={d}-info class="activityInfo">"""
# body += f"""<div class=activityInfoRow><span class="activityInfoTag">Distance: </span><span id="{d}-distance"></span></div>"""
# body += f"""<div class=activityInfoRow><span class="activityInfoTag">Elevation gain: </span><span id="{d}-elevationGain"></span></div>"""
# body += f"""<div class=activityInfoRow><span class="activityInfoTag">Duration: </span><span id="{d}-duration"></span></div>"""
if e is not None:
# body += "<br />"
body += f"""<div id="{d}-text">{e}</div>"""
body += "</div>"
body += "<br />"
body += f"""<div id="{d}" class="activityMap">"""
body += "</div>"
body += "<script>\n"
body += f"var map{count} = L.map('{d}');"
body += f"var controlElevation{count} = L.control.elevation(elevation_options).addTo(map{count});"
body += f"""controlElevation{count}.load("{l}");"""
body += """
L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
maxZoom: 17,
attribution: 'Map data: &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, <a href="http://viewfinderpanoramas.org">SRTM</a> | Map style: &copy; <a href="https://opentopomap.org">OpenTopoMap</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>)'"""
body += "})" + f".addTo(map{count});"
# body += f"var gpx{count} = '{l}';"
# body += f" new L.GPX(gpx{count}" + ", {"
# body += """
# async: true,
# marker_options: {
# startIconUrl: 'img/pin-icon-start.png',
# endIconUrl: 'img/pin-icon-end.png',
# shadowUrl: 'img/pin-shadow.png',
# //clickable: true,
# //showRouteInfo: true
# },
# }).on('loaded', function(e) {\n"""
# # body += "console.log(e.target);"
# body += f"map{count}.fitBounds(e.target.getBounds());\n"
# body += f"""addText((e.target.get_distance()/1000).toFixed(2) + " km", "{d}-distance");\n"""
# body += f"""addText(e.target.get_elevation_gain().toFixed(0) + " m", "{d}-elevationGain");\n"""
# body += f"""addText(new Date(e.target.get_moving_time()).toISOString().substr(11, 8), "{d}-duration");\n"""
# body += "})"
# body += f".addTo(map{count});"
body += "</script>\n"
# Add images
for image in im:
image_title = os.path.splitext(os.path.basename(image))[0]
image_title = image_title.replace("-", " ")
body += f"<a href=\"{image}\">"
body += f"<img src=\"{image}\" title=\"{image_title}\"/>"
body += "</a>"
body += "\n"
body += f"<figcaption>{image_title}</figcaption>"
body += "</section>"
# body += "<hr />"
body += "\n"
count += 1
body += "</section>"
body += "\n"
body += "</article>"
body += "\n"
page = self.combine_layouts(body)
self.save_page(page, f"activities-{period}.html")
if i == 0:
self.save_page(page, f"activities.html")
activities_pages.append([f"activities-{period}.html", period])
print(period)
def generate_rss(self):
with open("layouts/rssfeedtemplate.xml", "r") as f:
feed = f.read()
with open("layouts/rssitemtemplate.xml", "r") as f:
item_template = f.read()
items = ""
for l, t, d, c, i in zip(self.blog_links, self.blog_titles, self.blog_rfcdates, self.blog_contents, self.blog_image_links):
c = re.sub(r"<script(.|\n)+?script>", "", c)
c = re.sub(r"<link(.)+?>", "", c)
c = c.replace(
'src="posts', 'src="' + self.baseurl + 'posts'
)
guid = self.baseurl + l
if guid.startswith("https://erikjohannes.no/photofeed") or guid.startswith("https://erikjohannes.no/activities"):
guid = self.baseurl + i
c = c.replace(
'src="', 'src="' + self.baseurl
)
c = html.escape(c)
item = item_template.format(
t, self.baseurl + l, d, guid, c
)
items += item
date = datetime.datetime.now()
date = date - datetime.timedelta(hours=3)
rfcdate = utils.format_datetime(date)
feed = feed.format(rfcdate, items)
self.save_page(feed, "index.xml")
# def build_galleries(self):
# for f in os.listdir(self.photography_folder):
# if os.path.isdir(f):
# body = "<h1>Erik Johannes Husom's photography</h1>"
# body += "\n"
# body += "<h2>" + f + "</h2>"
# body += "\n"
# body += "<section class=gallerymasonry>"
# for img in os.listdir(self.photography_folder + "/" + f):
# if os.path.splitext(img)[1].lower() in self.img_exts:
# body += " <section class=galleryitem>"
# body +=
# body += "</section>"
# page = self.combine_layouts(body)
# self.save_page(page, f + ".html")
if __name__ == '__main__':
website = Website()
website.build_pages()
website.build_blog(exclude_drafts=True)
website.read_photo_feed("yearly")
website.create_activity_feed()
website.generate_rss()