1
0
mirror of https://github.com/venthur/blag.git synced 2025-11-25 20:52:43 +00:00
This commit is contained in:
Bastian Venthur
2021-01-11 22:36:30 +01:00
parent 4cd3afb08d
commit d3fe365bb0
11 changed files with 233 additions and 176 deletions

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
__pycache__/
*.py[cod]
build/
dist/
*.egg-info/
htmlcov/
.coverage
venv/

View File

@@ -1,24 +1,31 @@
.PHONY: \ VENV = venv
test \
lint \
docs \
release \
clean
all: lint test all: lint test
$(VENV): requirements.txt requirements-dev.txt setup.py
python3 -m venv $(VENV)
$(VENV)/bin/python -m pip install --upgrade -r requirements.txt
$(VENV)/bin/python -m pip install --upgrade -r requirements-dev.txt
$(VENV)/bin/python -m pip install -e .
touch $(VENV)
test: test:
pytest $(VENV)/bin/python -m pytest
.PHONY: test
lint: lint:
flake8 $(VENV)/bin/python -m flake8
.PHONY: lint
#docs:
# $(MAKE) -C docs html
release: release:
python3 setup.py sdist bdist_wheel upload python3 setup.py sdist bdist_wheel upload
.PHONY: release
clean: clean:
rm -rf $(VENV)
find . -type f -name *.pyc -delete find . -type f -name *.pyc -delete
find . -type d -name __pycache__ -delete find . -type d -name __pycache__ -delete
# coverage
rm -rf htmlcov .coverage
.PHONY: clean

3
requirements-dev.txt Normal file
View File

@@ -0,0 +1,3 @@
pytest==6.2.1
pytest-cov==2.10.1
flake8==3.8.4

View File

@@ -1 +1,4 @@
markdown==2.6.11 markdown==3.3.3
feedgenerator==1.9.1
jinja2==2.11.2
pygments==2.7.3

9
setup.cfg Normal file
View File

@@ -0,0 +1,9 @@
[tool:pytest]
addopts =
--cov=sg
--cov=tests
--cov-report=html
--cov-report=term-missing:skip-covered
[flake8]
exclude = venv,build

View File

@@ -20,25 +20,15 @@ setup(
python_requires='>=3', python_requires='>=3',
install_requires=[ install_requires=[
'markdown', 'markdown',
'feedgenerator',
'jinja2',
'pygments',
], ],
extras_require={
'dev': [
'pytest',
'pytest-cov',
'flake8',
]
},
packages=['sg'], packages=['sg'],
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [
'sg = sg.__main__:main' 'sg = sg.sg:main'
] ]
}, },
license='MIT', license='MIT',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3',
],
) )

View File

@@ -1,15 +0,0 @@
import logging
from sg import sg
def main():
logging.basicConfig(level=logging.DEBUG,
format="%(levelname)s\t%(message)s")
sg.prepare_site()
sg.copy_static_content()
sg.generate_site()
if __name__ == '__main__':
main()

255
sg/sg.py
View File

@@ -8,162 +8,151 @@
__author__ = "Bastian Venthur <venthur@debian.org>" __author__ = "Bastian Venthur <venthur@debian.org>"
import argparse
import os import os
import shutil import shutil
import string import string
import codecs import codecs
import re import re
import logging import logging
from datetime import datetime
import markdown import markdown
from jinja2 import Environment, FileSystemLoader
import feedgenerator
logger = logging.getLogger(__name__)
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s %(levelname)s %(name)s %(message)s',
)
LAYOUTS_DIR = '_layouts' def main(args=None):
RESULT_DIR = '_site' args = parse_args(args)
STATIC_DIR = '_static' args.func(args)
def parse_args(args):
"""Parse command line arguments.
Paramters
---------
args :
optional parameters, used for testing
Returns
-------
args
DEFAULT_LAYOUT = os.path.sep.join([LAYOUTS_DIR, 'default.html'])
DEFAULT_LAYOUT_HTML = """
<html>
<head></head>
<body>$content</body>
</html>
""" """
parser = argparse.ArgumentParser()
commands = parser.add_subparsers(dest='command')
commands.required = True
build_parser = commands.add_parser('build')
build_parser.set_defaults(func=build)
build_parser.add_argument(
'-i', '--input-dir',
default='content',
help='Input directory (default: content)',
)
build_parser.add_argument(
'-o', '--output-dir',
default='build',
help='Ouptut directory (default: build)',
)
return parser.parse_args()
def prepare_site(): def build(args):
"""Prepare site generation.""" os.makedirs(f'{args.output_dir}', exist_ok=True)
logging.info("Checking if all needed dirs and files are available.") convertibles = []
# check if all needed dirs and files are available for root, dirnames, filenames in os.walk(args.input_dir):
for directory in LAYOUTS_DIR, STATIC_DIR: for filename in filenames:
if not os.path.exists(directory): relpath = os.path.relpath(f'{root}/{filename}', start=args.input_dir)
logging.warning("Directory {} does not exist, creating it." abspath = os.path.abspath(f'{root}/{filename}')
.format(directory)) # all non-markdown files are just copied over, the markdown
os.mkdir(directory) # files are converted to html
if not os.path.exists(DEFAULT_LAYOUT): if abspath.endswith('.md'):
logging.warning("File {} does not exist, creating it." dstpath = os.path.abspath(f'{args.output_dir}/{relpath}')
.format(DEFAULT_LAYOUT)) dstpath = dstpath[:-3] + '.html'
filehandle = open(DEFAULT_LAYOUT, 'w') convertibles.append((abspath, dstpath))
filehandle.write(DEFAULT_LAYOUT_HTML) else:
filehandle.close() shutil.copy(abspath, f'{args.output_dir}/{relpath}')
# clean RESULT_DIR for dirname in dirnames:
shutil.rmtree(os.path.sep.join([os.curdir, RESULT_DIR]), True) # all directories are copied into the output directory
path = os.path.relpath(f'{root}/{dirname}', start=args.input_dir)
os.makedirs(f'{args.output_dir}/{path}', exist_ok=True)
convert_to_html(convertibles)
def generate_site(): def convert_to_html(convertibles):
"""Generate the dynamic part of the site."""
logging.info("Generating Site.")
for root, dirs, files in os.walk(os.curdir):
# ignore directories starting with _
if root.startswith(os.path.sep.join([os.curdir, '_'])):
continue
for f in files:
if f.endswith(".markdown"):
path = os.path.sep.join([root, f])
html = render_page(path)
filename = path.replace(".markdown", ".html")
save_page(dest_path(filename), html)
env = Environment(
loader=FileSystemLoader(['templates']),
)
def copy_static_content(): md = markdown.Markdown(
"""Copy the static content to RESULT_DIR.""" extensions=['meta', 'fenced_code', 'codehilite'],
logging.info("Copying static content.") output_format='html5',
shutil.copytree(os.path.sep.join([os.curdir, STATIC_DIR]), )
os.path.sep.join([os.curdir, RESULT_DIR]))
pages = []
articles = []
def save_page(path, txt): for src, dst in convertibles:
"""Save the txt under the given filename.""" logger.debug(f'Processing {src}')
# create directory if necessairy with open(src, 'r') as fh:
if not os.path.exists(os.path.dirname(path)): body = fh.read()
os.mkdir(os.path.dirname(path)) md.reset()
fh = codecs.open(path, 'w', 'utf-8') content = md.convert(body)
fh.write(txt) meta = md.Meta
fh.close() # convert markdown's weird format to str or list[str]
for key, value in meta.items():
value = '\n'.join(value).split(',')
def dest_path(path): value = [v.strip() for v in value]
"""Convert the destination path from the given path.""" if len(value) == 1:
base_dir = os.path.abspath(os.curdir) value = value[0]
path = os.path.abspath(path)
if not path.startswith(base_dir):
raise Exception("Path not in base_dir.")
path = path[len(base_dir):]
return os.path.sep.join([base_dir, RESULT_DIR, path])
def process_markdown(txt):
"""Convert given txt to html using markdown."""
html = markdown.markdown(txt)
return html
def process_embed_content(template, content):
"""Embedd content into html template."""
txt = string.Template(template)
html = txt.safe_substitute({'content': content})
return html
def process_embed_meta(template, content):
"""Embedd meta info into html template."""
txt = string.Template(template)
html = txt.safe_substitute(content)
return html
def get_meta(txt):
"""Parse meta information from text if available and return as dict.
meta information is a block imbedded in "---\n" lines having the format:
key: value
both are treated as strings the value starts after the ": " end ends with
the newline.
"""
SEP = '---\n'
meta = dict()
if txt.count(SEP) > 1 and txt.startswith(SEP):
stuff = txt[len(SEP):txt.find(SEP, 1)]
txt = txt[txt.find((SEP), 1)+len(SEP):]
for i in stuff.splitlines():
if i.count(':') > 0:
key, value = i.split(':', 1)
value = value.strip()
meta[key] = value meta[key] = value
return meta, txt
context = dict(content=content)
context.update(meta)
# for now, treat all pages as articles
if not meta:
pages.append((dst, context))
#template = env.get_template('page.html')
else:
articles.append((dst, context))
#template = env.get_template('article.html')
template = env.get_template('article.html')
result = template.render(context)
with open(dst, 'w') as fh_dest:
fh_dest.write(result)
def check_unused_variables(txt): # generate feed
"""Search for unused $foo variables and print a warning.""" feed = feedgenerator.Atom1Feed(
template = '\\$[_a-z][_a-z0=9]*' link='https://venthur.de',
f = re.findall(template, txt) title='my title',
if len(f) > 0: description='basti"s blag',
logging.warning("Unconsumed variables in template found: %s" % f) )
for dst, context in articles:
feed.add_item(
title=context['title'],
link=dst,
description=context['title'],
content=context['content'],
pubdate=datetime.fromisoformat(context['date']),
)
def render_page(path): with open('atom.xml', 'w') as fh:
"""Render page. feed.write(fh, encoding='utf8')
# generate archive
# generate tags
It starts with the file under path, and processes it by pushing it through if __name__ == '__main__':
the processing pipeline. It returns a string. main()
"""
logging.debug("Rendering %s" % path)
fh = codecs.open(path, 'r', 'utf-8')
txt = "".join(fh.readlines())
fh.close()
fh = codecs.open(DEFAULT_LAYOUT, 'r', 'utf-8')
template = ''.join(fh.readlines())
fh.close()
# get meta information
meta, txt = get_meta(txt)
# currently we only process markdown, other stuff can be added easyly
txt = process_markdown(txt)
txt = process_embed_content(template, txt)
txt = process_embed_meta(txt, meta)
check_unused_variables(txt)
return txt

25
templates/article.html Normal file
View File

@@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block title %}{{ title }}{% endblock %}
{% block head %}
{{ super() }}
{% endblock %}
{% block content %}
<p>Written on {{ date }}.</p>
{{ content }}
{% if tags %}
<p>This entry was tagged
{% for tag in tags|sort(case_sensitive=true) %}
{%- if not loop.first and not loop.last %}, {% endif -%}
{%- if loop.last and not loop.first %} and {% endif %}
<a href="/tag.html">{{ tag }}</a>
{%- endfor %}
</p>
{% endif %}
{% endblock %}

34
templates/base.html Normal file
View File

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<link rel="stylesheet" href="/style.css" type="text/css">
<link rel="stylesheet" href="/code.css" type="text/css">
<title>{% block title %}{% endblock %}</title>
</head>
<body>
<header>
<h1>Still don't have a title</h1>
<h2>-- and no tagline either</h2>
<h3>Bastian Venthur's Blog</h3>
<nav>
<ul>
<li><a href="/">Blog</a></li>
<li><a href="/archives.html">Archive</a></li>
<li><a href="/tags.html">Tags</a></li>
<li><a href="https://venthur.de/pages/about-me.html">About&nbsp;Me</a></li>
</ul>
</nav>
</header>
<main>
{% block content %}
{% endblock %}
</main>
</body>
</html>

1
templates/page.html Normal file
View File

@@ -0,0 +1 @@
{{ content }}