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 @@ ...@@ -2,30 +2,18 @@
import os import os
import sys import sys
import shutil
import distutils.dir_util import distutils.dir_util
import locale import locale
import yaml
import datetime
import uuid
import re
from enum import Enum
import markdown import boop.environment as environment
import pytz import boop.configuration as config
import boop.utils as utils
import boopsy from boop.feed import Feed
import boopfeed
locale.setlocale(locale.LC_ALL, "") locale.setlocale(locale.LC_ALL, "")
class Environment(Enum):
DEVELOPMENT = 1
PRODUCTION = 2
class ProgramError(Exception): class ProgramError(Exception):
"""Exception raised during the program execution. """Exception raised during the program execution.
...@@ -41,234 +29,14 @@ class ProgramError(Exception): ...@@ -41,234 +29,14 @@ class ProgramError(Exception):
pass 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): def main(environment):
# Make sure ./site folder is empty # Make sure ./site folder is empty
output_path = os.path.join(os.curdir, "site") 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) # Load the configuration (it can be overidden by a configuration.yml file)
configuration_path = os.path.join(os.curdir, "configuration.yml") 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 # STEP 1: we load all the Articles and Pages
pages = [] pages = []
...@@ -280,24 +48,20 @@ def main(environment): ...@@ -280,24 +48,20 @@ def main(environment):
if not os.path.exists(index_filepath): if not os.path.exists(index_filepath):
raise ProgramError("./index.html file does not exist") raise ProgramError("./index.html file does not exist")
with open(index_filepath, "r") as index_file: index_page = utils.build_page_from_filepath(
page = Page("index", index_file.read(), configuration=configuration) index_filepath, configuration, basename=os.curdir
pages.append(page) )
pages.append(index_page)
# Check that pages directory exists and load Pages from it # Check that pages directory exists and load Pages from it
pages_path = os.path.join(os.curdir, "pages") pages_path = os.path.join(os.curdir, "pages")
if os.path.isdir(pages_path): if os.path.isdir(pages_path):
for filename in dir_tree(pages_path): for filename in utils.dir_tree(pages_path, only=["html"]):
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
page_filepath = os.path.join(pages_path, filename) page_filepath = os.path.join(pages_path, filename)
with open(page_filepath, "r") as page_file: page = utils.build_page_from_filepath(
page = Page(slug, page_file.read(), configuration=configuration) page_filepath, configuration, basename=pages_path
pages.append(page) )
pages.append(page)
# Check that articles directory exists and load Articles from it # Check that articles directory exists and load Articles from it
articles_path = os.path.join(os.curdir, "articles") articles_path = os.path.join(os.curdir, "articles")
...@@ -305,178 +69,80 @@ def main(environment): ...@@ -305,178 +69,80 @@ def main(environment):
# We iterate on the directories of articles dir. If they contain # We iterate on the directories of articles dir. If they contain
# "serie.html" file, it means they contain a serie. Otherwise, we list # "serie.html" file, it means they contain a serie. Otherwise, we list
# articles as usual. # articles as usual.
for dirname in list_dirs(articles_path): dirs_with_articles = list(utils.list_dirs(articles_path)) + ["."]
dirpath = os.path.join(articles_path, dirname) 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_filepath = os.path.join(dirpath, "serie.html")
serie = None serie = None
if dirname != "." and os.path.exists(serie_filepath):
if os.path.exists(serie_filepath):
# We have a serie Page! Each article of the directory must be # We have a serie Page! Each article of the directory must be
# considered part of the serie. # considered part of the serie.
slug = f"serie/{dirname}" slug = f"serie/{dirname}"
with open(serie_filepath, "r") as serie_file: serie = utils.build_page_from_filepath(
serie = Page(slug, serie_file.read(), configuration=configuration) serie_filepath, configuration, slug=slug
pages.append(serie) )
pages.append(serie)
articles_by_series[serie] = [] articles_by_series[serie] = []
for filename in list_files(dirpath): for filename in utils.list_files(dirpath, only=["md"]):
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(dirpath, filename) article_filepath = os.path.join(dirpath, filename)
with open(article_filepath, "r") as article_file: article = utils.build_article_from_filepath(
article = Article( article_filepath, configuration, serie
slug,
article_file.read(),
configuration=configuration,
serie=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) articles.append(article)
if serie:
articles_by_series[serie].append(article)
# STEP 2: we write all the pages and articles # We want to sort the articles by publication date first
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
articles.sort(key=lambda article: article.date(), reverse=True) articles.sort(key=lambda article: article.date(), reverse=True)
for serie_articles in articles_by_series.values(): for serie_articles in articles_by_series.values():
serie_articles.sort(key=lambda article: article.date(), reverse=True) serie_articles.sort(key=lambda article: article.date(), reverse=True)
# And write them in the ./site folder # Load a "blog" page which has access to the list of articles
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
blog_template_filepath = os.path.join("templates", "blog.html") blog_template_filepath = os.path.join("templates", "blog.html")
if os.path.exists(blog_template_filepath): if os.path.exists(blog_template_filepath):
blog_template = boopsy.Template(blog_template_filepath) blog_page = utils.build_blog_page(articles, configuration)
context = {} pages.append(blog_page)
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)
feed_dirpath = os.path.join(output_path, "feeds") # STEP 2: we write all the pages and articles
os.mkdir(feed_dirpath) for page in pages:
utils.write_content(page, output_path)
# Build the main Atom feed print(f"Written page: {page.url()}")
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,
}
)
for article in articles: for article in articles:
# and complete it for each article utils.write_content(article, output_path)
entry = { print(f"Written article: {article.url()}")
"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 # STEP 3: we write also the Atom feeds (the main one + the series').
feed_filepath = os.path.join(feed_dirpath, "all.atom.xml") # We start by collecting the different feeds
feed.write(feed_filepath) feeds = []
print(f"Written feed: {site_url}feeds/all.atom.xml") 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(): for serie, serie_articles in articles_by_series.items():
feed_filename = f"{os.path.basename(serie.slug())}.atom.xml" # And the series' ones
feed_filepath = os.path.join(feed_dirpath, feed_filename) if len(serie_articles) == 0:
continue
feed = boopfeed.Atom()
feed.add_header( slug = f"feeds/{os.path.basename(serie.slug())}"
{ feed_title = f"{serie.title()} - {configuration['SITE_TITLE']}"
"title": f"{serie.title()} - {site_title}", feed = Feed(
"self_link": f"{site_url}feeds/{feed_filename}", slug,
"site_link": serie.url(), serie_articles,
"id": f"urn:uuid:{serie.uuid()}", {"title": feed_title, "site_link": serie.url()},
"updated": current_datetime, configuration=configuration,
}
) )
feeds.append(feed)
for article in serie_articles: if len(feeds) > 0:
# and complete it for each article # If we have collected feeds (i.e. we have at least one article), we
entry = { # write them.
"title": article.title(), for feed in feeds:
"id": f"urn:uuid:{article.uuid()}", utils.write_feed(feed, output_path)
"article_link": article.url(), print(f"Written feed: {feed.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}")
# STEP 4: and we finish by copying the static folder. # STEP 4: and we finish by copying the static folder.
static_dirpath = os.path.join(os.curdir, "static") static_dirpath = os.path.join(os.curdir, "static")
...@@ -486,10 +152,7 @@ def main(environment): ...@@ -486,10 +152,7 @@ def main(environment):
if __name__ == "__main__": if __name__ == "__main__":
environment = Environment.PRODUCTION environment = environment.load_from_argv(sys.argv)
if len(sys.argv) == 2 and sys.argv[1] == "--development":
environment = Environment.DEVELOPMENT
try: try:
main(environment) main(environment)
print("Boop!") 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]