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

568 lines
17 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.
5 years ago
# Modifed by Yax
5 years ago
"""Make static website/blog with Python."""
import argparse
import datetime
import json
import locale
5 years ago
import os
import re
import shutil
import sys
5 years ago
import time
import unicodedata
5 years ago
from email import utils
5 years ago
from pathlib import Path
import requests
import mistune
5 years ago
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import html
5 years ago
5 years ago
# set user locale
5 years ago
locale.setlocale(locale.LC_ALL, "")
5 years ago
# initialize markdown
class HighlightRenderer(mistune.HTMLRenderer):
def block_code(self, code, lang=None):
if lang:
lexer = get_lexer_by_name(lang, stripall=True)
formatter = html.HtmlFormatter()
return highlight(code, lexer, formatter)
return '<pre><code>' + mistune.escape(code) + '</code></pre>'
5 years ago
markdown = mistune.create_markdown(renderer=HighlightRenderer())
5 years ago
5 years ago
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
5 years ago
content["slug"] = slugify(content["title"])
5 years ago
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
summary_index = text.find("<!-- more")
if summary_index > 0:
summary = markdown(clean_html_tag(text[:summary_index]))
else:
summary = truncate(markdown(clean_html_tag(text)))
5 years ago
clean_text = text.replace("<!-- more -->", "")
5 years ago
text = markdown(clean_text)
5 years ago
else:
summary = truncate(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"]),
"summary": summary,
}
)
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_header_list_value(header_name, page_params):
l = []
if header_name in page_params:
for s in page_params[header_name].split(" "):
if s.strip():
l.append(s.strip())
return l
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, comment_detail_layout, **params
5 years ago
):
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)
5 years ago
# render text / summary for basic fields
content["content"] = render(content["content"], **params)
content["summary"] = render(content["summary"], **params)
5 years ago
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]
page_params["post_url"] = page_params["year"] + "/" + page_params["slug"] + "/"
5 years ago
5 years ago
# categories
5 years ago
categories = get_header_list_value("category", 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
# tags
5 years ago
tags = get_header_list_value("tag", page_params)
5 years ago
page_params["tags"] = tags
# comments
page_comment = page_params.get("comment", "yes")
if page_comment == "no":
is_page_comment_enabled = False
else:
is_page_comment_enabled = True
5 years ago
page_params["comment_count"] = 0
5 years ago
page_params["comments"] = ""
page_params["comment"] = ""
if params["stacosys_url"] and is_page_comment_enabled:
5 years ago
req_url = params["stacosys_url"] + "/comments"
5 years ago
query_params = dict(
url="/" + page_params["post_url"]
5 years ago
)
resp = requests.get(url=req_url, params=query_params)
5 years ago
comments = resp.json()["data"]
5 years ago
out_comments = []
for comment in comments:
site = comment.get("site", "")
5 years ago
if site:
site_start = '<a href="' + site + '">'
site_end = '</a>'
else:
site_start = ''
site_end = ''
5 years ago
out_comment = render(
comment_detail_layout,
5 years ago
author=comment["author"],
avatar=comment.get("avatar", ""),
5 years ago
site_start=site_start,
site_end=site_end,
5 years ago
date=comment["date"],
5 years ago
content=markdown(comment["content"]),
5 years ago
)
5 years ago
out_comments.append(out_comment)
page_params["comments"] = "".join(out_comments)
5 years ago
page_params["comment_count"] = len(comments)
page_params["comment"] = render(comment_layout, **page_params)
5 years ago
5 years ago
content["year"] = page_params["year"]
5 years ago
content["post_url"] = page_params["post_url"]
5 years ago
content["categories"] = page_params["categories"]
content["category_label"] = page_params["category_label"]
5 years ago
content["tags"] = page_params["tags"]
5 years ago
content["friendly_date"] = page_params["friendly_date"]
5 years ago
content["comment_count"] = page_params["comment_count"]
5 years ago
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
2 years ago
def make_notes(
src, src_pattern, dst, layout, **params
):
"""Generate notes from notes directory."""
items = []
for posix_path in Path(src).glob(src_pattern):
src_path = str(posix_path)
content = read_content(src_path)
# render text / summary for basic fields
content["content"] = render(content["content"], **params)
content["summary"] = render(content["summary"], **params)
page_params = dict(params, **content)
page_params["header"] = ""
page_params["footer"] = ""
page_params["friendly_date"] = ""
page_params["category_label"] = ""
page_params["post_url"] = "notes/" + page_params["slug"] + "/"
content["post_url"] = page_params["post_url"]
content["friendly_date"] = page_params["friendly_date"]
content["category_label"] = page_params["category_label"]
items.append(content)
dst_path = render(dst, **page_params)
output = render(layout, **page_params)
log("Rendering {} => {} ...", src_path, dst_path)
fwrite(dst_path, output)
return sorted(items, key=lambda x: x["date"], reverse=True)
5 years ago
def make_list(
posts, dst, list_layout, item_layout, header_layout, footer_layout, **params
5 years ago
):
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 "comment_count" in item_params and item_params["comment_count"]:
if item_params["comment_count"] == 1:
item_params["comment_label"] = "1 commentaire"
5 years ago
else:
5 years ago
item_params["comment_label"] = (
str(item_params["comment_count"]) + " commentaires"
5 years ago
)
5 years ago
else:
5 years ago
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(param_file):
5 years ago
# 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_url": "",
5 years ago
}
log("use params from " + param_file)
if os.path.isfile(param_file):
params.update(json.loads(fread(param_file)))
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")
item_note_layout = fread("layout/item_note.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")
comment_detail_layout = fread("layout/comment-detail.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")
2 years ago
note_layout = fread("layout/note.html")
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)
2 years ago
note_layout = render(page_layout, content=note_layout)
5 years ago
# Create blogs.
5 years ago
blog_posts = make_posts(
"posts",
"**/*.md",
5 years ago
"_site/{{ post_url }}/index.html",
5 years ago
post_layout,
category_layout,
5 years ago
comment_layout,
comment_detail_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
]
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) + "/"
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) + "/"
5 years ago
make_list(
chunk,
5 years ago
"_site/page" + str(page) + "/index.html",
5 years ago
list_layout,
item_layout,
banner_layout,
paging_layout,
**params
)
5 years ago
page = page + 1
5 years ago
# Create category pages
cat_post = {}
5 years ago
for post in blog_posts:
5 years ago
for cat in post["categories"]:
if cat in cat_post:
cat_post[cat].append(post)
5 years ago
else:
cat_post[cat] = [post]
for cat in cat_post.keys():
5 years ago
params["category"] = cat
make_list(
cat_post[cat],
5 years ago
"_site/" + slugify(cat) + "/index.html",
5 years ago
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,
5 years ago
"_site/archives/index.html",
5 years ago
list_layout,
item_nosummary_layout,
archive_title_layout,
None,
**params
)
5 years ago
# Create main RSS feed for 10 last entries
5 years ago
nb_items = min(10, len(blog_posts))
2 years ago
for filename in ("_site/rss.xml", "_site/index.xml"):
make_list(
blog_posts[:nb_items],
filename,
rss_xml,
rss_item_xml,
None,
None,
**params
)
5 years ago
5 years ago
# Create RSS feed by tag
tag_post = {}
5 years ago
for post in blog_posts:
for tag in post["tags"]:
if tag in tag_post:
tag_post[tag].append(post)
5 years ago
else:
tag_post[tag] = [post]
for tag in tag_post.keys():
5 years ago
params["tag"] = tag
make_list(
tag_post[tag],
5 years ago
"_site/rss." + slugify(tag) + ".xml",
rss_xml,
rss_item_xml,
None,
None,
**params
)
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
2 years ago
# Create notes.
notes = make_notes(
"notes",
"**/*.md",
"_site/{{ post_url }}/index.html",
note_layout,
**params
)
make_list(
notes,
"_site/notes/index.html",
list_layout,
item_note_layout,
2 years ago
archive_title_layout,
None,
**params
)
5 years ago
# Test parameter to be set temporarily by unit tests.
_test = None
5 years ago
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Makesite')
parser.add_argument('--params', dest='param_file', type=str, default="params.json", help='Custom param file')
args = parser.parse_args()
main(args.param_file)