Commit 2210ce4e authored by berumuron's avatar berumuron
Browse files

tec: This is the final refactor

... I hope.
parents 694c4e3f 7bfac8f4
......@@ -2,30 +2,18 @@
import os
import sys
import shutil
import distutils.dir_util
import locale
import yaml
import datetime
import uuid
import re
from enum import Enum
import markdown
import pytz
import boopsy
import boopfeed
import boop.environment as environment
import boop.configuration as config
import boop.utils as utils
from boop.feed import Feed
locale.setlocale(locale.LC_ALL, "")
class Environment(Enum):
DEVELOPMENT = 1
PRODUCTION = 2
class ProgramError(Exception):
"""Exception raised during the program execution.
......@@ -41,234 +29,14 @@ class ProgramError(Exception):
pass
class Article:
def __init__(self, slug, article_content, configuration={}, serie=None):
# We convert the file's content into HTML
md = markdown.Markdown(extensions=["meta", "fenced_code", "attr_list"])
content = md.convert(article_content)
# We get the local variables from the Markdown file (metadata)
# which will be accessible in the article template
self.meta = {}
self.meta.update(configuration)
self.meta["ARTICLE_SLUG"] = slug
self.meta["ARTICLE_TEMPLATE"] = "article"
for key, values in md.Meta.items():
article_key = f"ARTICLE_{key.upper()}"
# The Markdown's meta extension extract all the values in arrays, even
# if there is only one value.
if len(values) > 1:
self.meta[article_key] = values
else:
self.meta[article_key] = values[0]
self.meta["ARTICLE_CONTENT"] = content
self.meta["ARTICLE_SERIE"] = serie
# And we initialize a template for the article
article_template_filepath = os.path.join(
"templates", f"{self.meta['ARTICLE_TEMPLATE']}.html"
)
self.template = boopsy.Template(article_template_filepath)
def render(self):
return self.template.render(self.meta)
def slug(self):
return self.meta["ARTICLE_SLUG"]
def title(self):
return self.meta.get("ARTICLE_TITLE", "An article")
def author(self):
if "ARTICLE_AUTHOR" in self.meta:
return self.meta["ARTICLE_AUTHOR"]
elif "SITE_AUTHOR" in self.meta:
return self.meta["SITE_AUTHOR"]
else:
return None
def url(self):
site_url = self.meta.get("SITE_URL", "")
return f"{site_url}{self.slug()}.html"
def uuid(self):
return str(uuid.uuid5(uuid.NAMESPACE_URL, self.url()))
def content(self):
return self.meta["ARTICLE_CONTENT"]
def timezone(self):
site_timezone = self.meta.get("SITE_TIMEZONE", "UTC")
return pytz.timezone(site_timezone)
def date(self):
if "ARTICLE_DATE" not in self.meta:
return datetime.datetime.now(tz=self.timezone())
article_date = datetime.datetime.strptime(
self.meta["ARTICLE_DATE"], "%Y-%m-%d %H:%M"
)
return article_date.astimezone(tz=self.timezone())
def update(self):
if "ARTICLE_UPDATE" not in self.meta:
return None
article_update = datetime.datetime.strptime(
self.meta["ARTICLE_UPDATE"], "%Y-%m-%d %H:%M"
)
return article_update.astimezone(tz=self.timezone())
def serie(self):
return self.meta["ARTICLE_SERIE"]
class Page:
def __init__(self, slug, page_content, configuration={}):
# We initialize the page meta variables which will be accessible in the
# template
self.meta = {}
self.meta.update(configuration)
self.meta["PAGE_SLUG"] = slug
self.meta["PAGE_TEMPLATE"] = "page"
yaml_conf, page_content = extract_yaml_configuration(page_content)
for key, value in yaml_conf.items():
article_key = f"PAGE_{key.upper()}"
self.meta[article_key] = value
self.meta["PAGE_CONTENT"] = page_content
# And we initialize a template for the page
page_template_filepath = os.path.join(
"templates", f"{self.meta['PAGE_TEMPLATE']}.html"
)
self.template = None
if os.path.exists(page_template_filepath):
self.template = boopsy.Template(page_template_filepath)
def render(self):
if self.template is None:
return self.content()
else:
return self.template.render(self.meta)
def slug(self):
return self.meta["PAGE_SLUG"]
def title(self):
return self.meta.get("PAGE_TITLE", "A page")
def url(self):
site_url = self.meta.get("SITE_URL", "")
return f"{site_url}{self.slug()}.html"
def uuid(self):
return str(uuid.uuid5(uuid.NAMESPACE_URL, self.url()))
def content(self):
return self.meta["PAGE_CONTENT"]
YAML_FRONT_MATTER_REGEX = re.compile("(---\n.*\n)(---\n)(.*)", re.M | re.S)
def extract_yaml_configuration(content):
match = YAML_FRONT_MATTER_REGEX.match(content)
if match:
yaml_conf = yaml.load(match.group(1))
return yaml_conf, match.group(3)
else:
return {}, content
def list_dirs(path):
"""List all the directories within a path.
"""
for filename in os.listdir(path):
full_path = os.path.join(path, filename)
if os.path.isdir(full_path):
yield filename
def list_files(path):
"""List all the files within a path.
"""
for filename in os.listdir(path):
full_path = os.path.join(path, filename)
if os.path.isfile(full_path):
yield filename
def dir_tree(path):
"""List all the tree directory under the given path.
"""
for filename in os.listdir(path):
full_path = os.path.join(path, filename)
if os.path.isdir(full_path):
for sub_filename in dir_tree(full_path):
yield os.path.join(filename, sub_filename)
else:
yield filename
def mkdirs_for_file(filepath):
"""Create parent directories for a given file.
"""
path = os.path.dirname(filepath)
os.makedirs(path, exist_ok=True)
def init_output_folder(output_path):
"""Create the output directory, empty it if it contains files.
"""
if os.path.isdir(output_path):
# it should be a directory...
shutil.rmtree(output_path)
elif os.path.exists(output_path):
# ... but it also can be a simple file if user created it manually!
os.remove(output_path)
os.mkdir(output_path)
def load_configuration(configuration_path, output_path):
configuration = {
"SITE_URL": "filesystem",
"SITE_TITLE": "A website",
"SITE_TIMEZONE": "UTC",
}
if os.path.exists(configuration_path):
with open(configuration_path) as conf_file:
conf_data = yaml.load(conf_file)
for key, value in conf_data.items():
if key == "development":
continue
site_key = f"SITE_{key.upper()}"
configuration[site_key] = value
if environment == Environment.DEVELOPMENT and "development" in conf_data:
for key, value in conf_data["development"].items():
site_key = f"SITE_{key.upper()}"
configuration[site_key] = value
configuration["SITE_UUID"] = str(
uuid.uuid5(uuid.NAMESPACE_URL, configuration["SITE_URL"])
)
if "SITE_AUTHOR" not in configuration:
configuration["SITE_AUTHOR"] = f"{configuration['SITE_TITLE']}'s author"
if configuration["SITE_URL"] == "filesystem":
configuration["SITE_URL"] = f"file://{os.path.abspath(output_path)}/"
return configuration
def main(environment):
# Make sure ./site folder is empty
output_path = os.path.join(os.curdir, "site")
init_output_folder(output_path)
utils.init_output_folder(output_path)
# Load the configuration (it can be overidden by a configuration.yml file)
configuration_path = os.path.join(os.curdir, "configuration.yml")
configuration = load_configuration(configuration_path, output_path)
configuration = config.load(configuration_path, environment, output_path)
# STEP 1: we load all the Articles and Pages
pages = []
......@@ -280,23 +48,19 @@ def main(environment):
if not os.path.exists(index_filepath):
raise ProgramError("./index.html file does not exist")
with open(index_filepath, "r") as index_file:
page = Page("index", index_file.read(), configuration=configuration)
pages.append(page)
index_page = utils.build_page_from_filepath(
index_filepath, configuration, basename=os.curdir
)
pages.append(index_page)
# Check that pages directory exists and load Pages from it
pages_path = os.path.join(os.curdir, "pages")
if os.path.isdir(pages_path):
for filename in dir_tree(pages_path):
slug, ext = os.path.splitext(filename)
if ext != ".html":
# Don't consider files which are not HTML
continue
# We read the page file and initialize a Page from it
for filename in utils.dir_tree(pages_path, only=["html"]):
page_filepath = os.path.join(pages_path, filename)
with open(page_filepath, "r") as page_file:
page = Page(slug, page_file.read(), configuration=configuration)
page = utils.build_page_from_filepath(
page_filepath, configuration, basename=pages_path
)
pages.append(page)
# Check that articles directory exists and load Articles from it
......@@ -305,178 +69,80 @@ def main(environment):
# We iterate on the directories of articles dir. If they contain
# "serie.html" file, it means they contain a serie. Otherwise, we list
# articles as usual.
for dirname in list_dirs(articles_path):
dirpath = os.path.join(articles_path, dirname)
dirs_with_articles = list(utils.list_dirs(articles_path)) + ["."]
for dirname in dirs_with_articles:
dirpath = os.path.realpath(os.path.join(articles_path, dirname))
serie_filepath = os.path.join(dirpath, "serie.html")
serie = None
if os.path.exists(serie_filepath):
if dirname != "." and os.path.exists(serie_filepath):
# We have a serie Page! Each article of the directory must be
# considered part of the serie.
slug = f"serie/{dirname}"
with open(serie_filepath, "r") as serie_file:
serie = Page(slug, serie_file.read(), configuration=configuration)
serie = utils.build_page_from_filepath(
serie_filepath, configuration, slug=slug
)
pages.append(serie)
articles_by_series[serie] = []
for filename in list_files(dirpath):
filename_no_ext, ext = os.path.splitext(filename)
slug = os.path.basename(filename_no_ext)
if ext != ".md":
continue
# We read the article file and initialize an Article from it
for filename in utils.list_files(dirpath, only=["md"]):
article_filepath = os.path.join(dirpath, filename)
with open(article_filepath, "r") as article_file:
article = Article(
slug,
article_file.read(),
configuration=configuration,
serie=serie,
article = utils.build_article_from_filepath(
article_filepath, configuration, serie
)
articles.append(article)
if serie:
articles_by_series[serie].append(article)
for filename in list_files(articles_path):
filename_no_ext, ext = os.path.splitext(filename)
slug = os.path.basename(filename_no_ext)
if ext != ".md":
continue
# We read the article file and initialize an Article from it
article_filepath = os.path.join(articles_path, filename)
with open(article_filepath, "r") as article_file:
article = Article(
slug, article_file.read(), configuration=configuration
)
articles.append(article)
# STEP 2: we write all the pages and articles
for page in pages:
output_filepath = os.path.join(output_path, f"{page.slug()}.html")
# Create dir structure for the file
mkdirs_for_file(output_filepath)
# And write the file
with open(output_filepath, "w") as output_file:
output_file.write(page.render())
print(f"Written page: {page.url()}")
# Sort the articles by publication date
# We want to sort the articles by publication date first
articles.sort(key=lambda article: article.date(), reverse=True)
for serie_articles in articles_by_series.values():
serie_articles.sort(key=lambda article: article.date(), reverse=True)
# And write them in the ./site folder
for article in articles:
output_filepath = os.path.join(output_path, f"{article.slug()}.html")
with open(output_filepath, "w") as output_file:
output_file.write(article.render())
print(f"Written article: {article.url()}")
# Write the page with the list of articles if blog template exists
# Load a "blog" page which has access to the list of articles
blog_template_filepath = os.path.join("templates", "blog.html")
if os.path.exists(blog_template_filepath):
blog_template = boopsy.Template(blog_template_filepath)
context = {}
context.update(configuration)
context["ARTICLES"] = articles
blog_output_filepath = os.path.join(output_path, f"blog.html")
with open(blog_output_filepath, "w") as output_file:
output_file.write(blog_template.render(context))
print(f"Written page: {configuration['SITE_URL']}blog.html")
# STEP 3: we write also the Atom feeds (the main one + the series').
# Just create some variables so it's easier to manipulate after
site_url = configuration["SITE_URL"]
site_uuid = configuration["SITE_UUID"]
site_title = configuration["SITE_TITLE"]
site_timezone = configuration["SITE_TIMEZONE"]
timezone = pytz.timezone(site_timezone)
current_datetime = datetime.datetime.now(tz=timezone)
blog_page = utils.build_blog_page(articles, configuration)
pages.append(blog_page)
feed_dirpath = os.path.join(output_path, "feeds")
os.mkdir(feed_dirpath)
# Build the main Atom feed
feed = boopfeed.Atom()
feed.add_header(
{
"title": site_title,
"self_link": f"{site_url}feeds/all.atom.xml",
"site_link": site_url,
"id": f"urn:uuid:{site_uuid}",
"updated": current_datetime,
}
)
# STEP 2: we write all the pages and articles
for page in pages:
utils.write_content(page, output_path)
print(f"Written page: {page.url()}")
for article in articles:
# and complete it for each article
entry = {
"title": article.title(),
"id": f"urn:uuid:{article.uuid()}",
"article_link": article.url(),
"published": article.date(),
"content": article.content(),
}
article_author = article.author()
if article_author:
entry["author"] = article_author
article_update = article.update()
if article_update:
entry["updated"] = article_update
feed.add_entry(entry)
utils.write_content(article, output_path)
print(f"Written article: {article.url()}")
# And write it on the filesystem
feed_filepath = os.path.join(feed_dirpath, "all.atom.xml")
feed.write(feed_filepath)
print(f"Written feed: {site_url}feeds/all.atom.xml")
# STEP 3: we write also the Atom feeds (the main one + the series').
# We start by collecting the different feeds
feeds = []
if len(articles) > 0:
# The main one...
feed = Feed("feeds/all", articles, configuration=configuration)
feeds.append(feed)
# Now, build the feeds for series (same process)
for serie, serie_articles in articles_by_series.items():
feed_filename = f"{os.path.basename(serie.slug())}.atom.xml"
feed_filepath = os.path.join(feed_dirpath, feed_filename)
# And the series' ones
if len(serie_articles) == 0:
continue
feed = boopfeed.Atom()
feed.add_header(
{
"title": f"{serie.title()} - {site_title}",
"self_link": f"{site_url}feeds/{feed_filename}",
"site_link": serie.url(),
"id": f"urn:uuid:{serie.uuid()}",
"updated": current_datetime,
}
slug = f"feeds/{os.path.basename(serie.slug())}"
feed_title = f"{serie.title()} - {configuration['SITE_TITLE']}"
feed = Feed(
slug,
serie_articles,
{"title": feed_title, "site_link": serie.url()},
configuration=configuration,
)
feeds.append(feed)
for article in serie_articles:
# and complete it for each article
entry = {
"title": article.title(),
"id": f"urn:uuid:{article.uuid()}",
"article_link": article.url(),
"published": article.date(),
"content": article.content(),
}
article_author = article.author()
if article_author:
entry["author"] = article_author
article_update = article.update()
if article_update:
entry["updated"] = article_update
feed.add_entry(entry)
# And write it on the filesystem
feed.write(feed_filepath)
print(f"Written feed: {site_url}feeds/{feed_filename}")
if len(feeds) > 0:
# If we have collected feeds (i.e. we have at least one article), we
# write them.
for feed in feeds:
utils.write_feed(feed, output_path)
print(f"Written feed: {feed.url()}")
# STEP 4: and we finish by copying the static folder.
static_dirpath = os.path.join(os.curdir, "static")
......@@ -486,10 +152,7 @@ def main(environment):
if __name__ == "__main__":
environment = Environment.PRODUCTION
if len(sys.argv) == 2 and sys.argv[1] == "--development":
environment = Environment.DEVELOPMENT
environment = environment.load_from_argv(sys.argv)
try:
main(environment)
print("Boop!")
......
import markdown
import datetime
import uuid
import pytz
class Article:
def __init__(self, slug, article_content, configuration={}, serie=None):
# We convert the file's content into HTML
md = markdown.Markdown(extensions=["meta", "fenced_code", "attr_list"])
content = md.convert(article_content)
# We get the local variables from the Markdown file (metadata)
# which will be accessible in the article template
self.meta = {}
self.meta.update(configuration)
self.meta["ARTICLE_SLUG"] = slug
self.meta["ARTICLE_TEMPLATE"] = "article"
for key, values in md.Meta.items():
article_key = f"ARTICLE_{key.upper()}"
# The Markdown's meta extension extract all the values in arrays, even
# if there is only one value.
if len(values) > 1:
self.meta[article_key] = values
else:
self.meta[article_key] = values[0]
self.meta["ARTICLE_CONTENT"] = content
self.meta["ARTICLE_SERIE"] = serie
def render(self):
return self.template.render(self.meta)
def slug(self):
return self.meta["ARTICLE_SLUG"]
def title(self):
return self.meta.get("ARTICLE_TITLE", "An article")
def author(self):
if "ARTICLE_AUTHOR" in self.meta:
return self.meta["ARTICLE_AUTHOR"]
elif "SITE_AUTHOR" in self.meta:
return self.meta["SITE_AUTHOR"]
else:
return None
def url(self):
site_url = self.meta.get("SITE_URL", "")
return f"{site_url}{self.slug()}.html"
def uuid(self):
return str(uuid.uuid5(uuid.NAMESPACE_URL, self.url()))
def content(self):
return self.meta["ARTICLE_CONTENT"]
def timezone(self):
site_timezone = self.meta.get("SITE_TIMEZONE", "UTC")
return pytz.timezone(site_timezone)
def date(self):
if "ARTICLE_DATE" not in self.meta:
return datetime.datetime.now(tz=self.timezone())
article_date = datetime.datetime.strptime(
self.meta["ARTICLE_DATE"], "%Y-%m-%d %H:%M"
)
return article_date.astimezone(tz=self.timezone())
def update(self):
if "ARTICLE_UPDATE" not in self.meta:
return None
article_update = datetime.datetime.strptime(
self.meta["ARTICLE_UPDATE"], "%Y-%m-%d %H:%M"
)
return article_update.astimezone(tz=self.timezone())