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()

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

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 }}