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

450 lines
13 KiB
Python

5 years ago
#!/usr/bin/env python3
5 years ago
# The MIT License (MIT)
#
# Copyright (c) 2018 Sunaina Pai
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""Make static website/blog with Python."""
5 years ago
import sys
5 years ago
import os
import shutil
import re
import glob
import json
import datetime
5 years ago
import time
from email import utils
5 years ago
from pathlib import Path
5 years ago
import unicodedata
5 years ago
import locale
5 years ago
import requests
import commonmark
5 years ago
# set user locale
5 years ago
locale.setlocale(locale.LC_ALL, "")
5 years ago
def fread(filename):
"""Read file and close the file."""
5 years ago
with open(filename, "r") as f:
5 years ago
return f.read()
def fwrite(filename, text):
"""Write content to file and close the file."""
basedir = os.path.dirname(filename)
if not os.path.isdir(basedir):
os.makedirs(basedir)
5 years ago
with open(filename, "w") as f:
5 years ago
f.write(text)
def log(msg, *args):
"""Log message with specified arguments."""
5 years ago
sys.stderr.write(msg.format(*args) + "\n")
5 years ago
def truncate(text, words=25):
"""Remove tags and truncate text to the specified number of words."""
5 years ago
return " ".join(re.sub("(?s)<.*?>", " ", text).split()[:words])
5 years ago
def read_headers(text):
"""Parse headers in text and yield (key, value, end-index) tuples."""
5 years ago
for match in re.finditer(r"\s*<!--\s*(.+?)\s*:\s*(.+?)\s*-->\s*|.+", text):
5 years ago
if not match.group(1):
break
yield match.group(1), match.group(2), match.end()
def rfc_2822_format(date_str):
"""Convert yyyy-mm-dd date string to RFC 2822 format date string."""
5 years ago
d = datetime.datetime.strptime(date_str, "%Y-%m-%d")
5 years ago
dtuple = d.timetuple()
dtimestamp = time.mktime(dtuple)
5 years ago
return utils.formatdate(dtimestamp)
5 years ago
5 years ago
def slugify(value):
"""
Converts to lowercase, removes non-word characters (alphanumerics and
underscores) and converts spaces to hyphens. Also strips leading and
trailing whitespace.
"""
5 years ago
value = (
unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii")
)
value = re.sub("[^\w\s-]", "", value).strip().lower()
return re.sub("[-\s]+", "-", value)
5 years ago
5 years ago
5 years ago
def read_content(filename):
"""Read content and metadata from file into a dictionary."""
# Read file content.
text = fread(filename)
# Read metadata and save it in a dictionary.
5 years ago
date_slug = os.path.basename(filename).split(".")[0]
match = re.search(r"^(?:(\d\d\d\d-\d\d-\d\d)-)?(.+)$", date_slug)
content = {"date": match.group(1) or "1970-01-01", "slug": match.group(2)}
5 years ago
# Read headers.
end = 0
for key, val, end in read_headers(text):
content[key] = val
5 years ago
# slugify post title
content['slug'] = slugify(content['title'])
5 years ago
# Separate content from headers.
text = text[end:]
# Convert Markdown content to HTML.
5 years ago
if filename.endswith((".md", ".mkd", ".mkdn", ".mdown", ".markdown")):
5 years ago
text = commonmark.commonmark(text)
5 years ago
# Update the dictionary with content and RFC 2822 date.
5 years ago
content.update({"content": text, "rfc_2822_date": rfc_2822_format(content["date"])})
5 years ago
return content
5 years ago
def clean_html_tag(text):
"""Remove HTML tags."""
while True:
original_text = text
5 years ago
text = re.sub("<\w+.*?>", "", text)
text = re.sub("<\/\w+>", "", text)
5 years ago
if original_text == text:
break
return text
5 years ago
def render(template, **params):
"""Replace placeholders in template with values from params."""
5 years ago
return re.sub(
r"{{\s*([^}\s]+)\s*}}",
lambda match: str(params.get(match.group(1), match.group(0))),
template,
)
5 years ago
5 years ago
def get_categories(page_params):
cat = []
5 years ago
for s in page_params["category"].split(" "):
5 years ago
if s.strip():
cat.append(s.strip())
5 years ago
return cat
5 years ago
def make_pages(src, dst, layout, **params):
"""Generate pages from page content."""
items = []
for src_path in glob.glob(src):
content = read_content(src_path)
page_params = dict(params, **content)
items.append(content)
dst_path = render(dst, **page_params)
output = render(layout, **page_params)
5 years ago
log("Rendering {} => {} ...", src_path, dst_path)
5 years ago
fwrite(dst_path, output)
5 years ago
return sorted(items, key=lambda x: x["date"], reverse=True)
5 years ago
5 years ago
def get_friendly_date(date_str):
5 years ago
dt = datetime.datetime.strptime(date_str, "%Y-%m-%d")
return dt.strftime("%d %b %Y")
5 years ago
5 years ago
def make_posts(src, src_pattern, dst, layout, category_layout, comment_layout, **params):
5 years ago
"""Generate posts from posts directory."""
items = []
for posix_path in Path(src).glob(src_pattern):
src_path = str(posix_path)
content = read_content(src_path)
page_params = dict(params, **content)
5 years ago
page_params["header"] = ""
page_params["footer"] = ""
page_params["date_path"] = page_params["date"].replace("-", "/")
page_params["friendly_date"] = get_friendly_date(page_params["date"])
page_params["year"] = page_params["date"].split("-")[0]
5 years ago
5 years ago
# categories
categories = get_categories(page_params)
5 years ago
out_cats = []
for category in categories:
5 years ago
out_cat = render(category_layout, category=category, url=slugify(category))
5 years ago
out_cats.append(out_cat.strip())
5 years ago
page_params["categories"] = categories
page_params["category_label"] = "".join(out_cats)
5 years ago
5 years ago
summary_index = page_params["content"].find("<!-- more")
5 years ago
if summary_index > 0:
5 years ago
content["summary"] = clean_html_tag(
render(page_params["content"][:summary_index], **page_params)
)
5 years ago
# stacosys comments
page_params['comment_count'] = 0
if params['stacosys_url']:
req_url = params['stacosys_url'] + '/comments'
query_params = dict(
token=params['stacosys_token'],
url='/' + page_params['year'] + '/' + page_params['slug']
)
resp = requests.get(url=req_url, params=query_params)
comments = resp.json()['data']
out_comments = []
for comment in comments:
out_comment = render(comment_layout, author=comment['author'], avatar=comment.get('avatar',''), site=comment.get('site', ''),
date=comment['date'], content=commonmark.commonmark(comment['content']))
out_comments.append(out_comment)
page_params["comments"] = "".join(out_comments)
page_params['comment_count'] = len(comments)
5 years ago
content["year"] = page_params["year"]
content["categories"] = page_params["categories"]
content["category_label"] = page_params["category_label"]
content["friendly_date"] = page_params["friendly_date"]
5 years ago
content["comment_count"] = page_params["comment_count"]
5 years ago
items.append(content)
5 years ago
# TODO DEBUG
5 years ago
# print(page_params)
# print(content)
# break
5 years ago
5 years ago
dst_path = render(dst, **page_params)
output = render(layout, **page_params)
5 years ago
log("Rendering {} => {} ...", src_path, dst_path)
5 years ago
fwrite(dst_path, output)
5 years ago
return sorted(items, key=lambda x: x["date"], reverse=True)
5 years ago
5 years ago
def make_list(
posts, dst, list_layout, item_layout, header_layout, footer_layout, **params
):
5 years ago
"""Generate list page for a blog."""
5 years ago
# header
if header_layout is None:
params["header"] = ""
else:
header = render(header_layout, **params)
params["header"] = header
# footer
if footer_layout is None:
params["footer"] = ""
else:
footer = render(footer_layout, **params)
params["footer"] = footer
# content
5 years ago
items = []
for post in posts:
item_params = dict(params, **post)
5 years ago
if "summary" not in item_params:
item_params["summary"] = truncate(post["content"])
5 years ago
if "comment_count" in item_params and item_params['comment_count']:
if item_params['comment_count'] == 1:
item_params['comment_label'] = '1 commentaire'
else:
item_params['comment_label'] = str(item_params['comment_count']) + ' commentaires'
else:
item_params['comment_label'] = ''
5 years ago
item = render(item_layout, **item_params)
items.append(item)
5 years ago
params["content"] = "".join(items)
5 years ago
dst_path = render(dst, **params)
output = render(list_layout, **params)
5 years ago
log("Rendering list => {} ...", dst_path)
5 years ago
fwrite(dst_path, output)
def main():
# Create a new _site directory from scratch.
5 years ago
if os.path.isdir("_site"):
shutil.rmtree("_site")
shutil.copytree("static", "_site")
5 years ago
# Default parameters.
params = {
5 years ago
"title": "Blog",
"subtitle": "Lorem Ipsum",
"author": "Admin",
"site_url": "http://localhost:8000",
"current_year": datetime.datetime.now().year,
5 years ago
"stacosys_token": "",
"stacosys_url": ""
5 years ago
}
# If params.json exists, load it.
5 years ago
if os.path.isfile("params.json"):
params.update(json.loads(fread("params.json")))
5 years ago
# Load layouts.
5 years ago
banner_layout = fread("layout/banner.html")
paging_layout = fread("layout/paging.html")
5 years ago
archive_title_layout = fread("layout/archives.html")
5 years ago
page_layout = fread("layout/page.html")
post_layout = fread("layout/post.html")
list_layout = fread("layout/list.html")
item_layout = fread("layout/item.html")
5 years ago
item_nosummary_layout = fread("layout/item_nosummary.html")
5 years ago
category_title_layout = fread("layout/category_title.html")
5 years ago
category_layout = fread("layout/category.html")
5 years ago
comment_layout = fread("layout/comment.html")
5 years ago
rss_xml = fread("layout/rss.xml")
rss_item_xml = fread("layout/rss_item.xml")
sitemap_xml = fread("layout/sitemap.xml")
sitemap_item_xml = fread("layout/sitemap_item.xml")
5 years ago
5 years ago
# Combine layouts to form final layouts.
post_layout = render(page_layout, content=post_layout)
5 years ago
list_layout = render(page_layout, content=list_layout)
5 years ago
# Create blogs.
5 years ago
blog_posts = make_posts(
"posts",
"**/*.md",
"_site/{{ year }}/{{ slug }}.html",
post_layout,
category_layout,
5 years ago
comment_layout,
5 years ago
**params
)
5 years ago
5 years ago
# Create blog list pages.
5 years ago
page_size = 10
5 years ago
chunk_posts = [
blog_posts[i : i + page_size] for i in range(0, len(blog_posts), page_size)
]
5 years ago
page = 1
5 years ago
last_page = len(chunk_posts)
for chunk in chunk_posts:
params["page"] = page
if page == last_page:
5 years ago
params["next_page"] = ""
5 years ago
else:
5 years ago
params["next_page"] = "page" + str(page + 1) + ".html"
5 years ago
if page == 1:
5 years ago
params["previous_page"] = ""
5 years ago
make_list(
chunk,
"_site/index.html",
list_layout,
item_layout,
banner_layout,
paging_layout,
**params
)
else:
5 years ago
params["previous_page"] = "page" + str(page - 1) + ".html"
5 years ago
make_list(
chunk,
"_site/page" + str(page) + ".html",
list_layout,
item_layout,
banner_layout,
paging_layout,
**params
)
5 years ago
page = page + 1
5 years ago
# Create category pages
catpost = {}
for post in blog_posts:
5 years ago
for cat in post["categories"]:
5 years ago
if cat in catpost:
catpost[cat].append(post)
else:
catpost[cat] = [post]
5 years ago
for cat in catpost.keys():
params["category"] = cat
make_list(
catpost[cat],
"_site/" + slugify(cat) + ".html",
list_layout,
5 years ago
item_nosummary_layout,
5 years ago
category_title_layout,
None,
**params
)
5 years ago
5 years ago
# Create archive page
make_list(
blog_posts,
"_site/archives.html",
list_layout,
item_nosummary_layout,
archive_title_layout,
None,
**params
)
5 years ago
# Create RSS feeds.
5 years ago
nb_items = min(10, len(blog_posts))
5 years ago
make_list(
blog_posts[:nb_items],
"_site/rss.xml",
rss_xml,
rss_item_xml,
None,
None,
**params
)
5 years ago
5 years ago
# Create sitemap
5 years ago
make_list(
blog_posts,
"_site/sitemap.xml",
sitemap_xml,
sitemap_item_xml,
None,
None,
**params
)
5 years ago
# Test parameter to be set temporarily by unit tests.
_test = None
5 years ago
if __name__ == "__main__":
5 years ago
main()