mirror of
https://github.com/venthur/blag.git
synced 2025-11-25 20:52:43 +00:00
Merge branch 'master' into mkdocs
This commit is contained in:
34
CHANGELOG.md
34
CHANGELOG.md
@@ -14,6 +14,40 @@
|
|||||||
New users are not affected as `blag quickstart` will generate the needed
|
New users are not affected as `blag quickstart` will generate the needed
|
||||||
templates.
|
templates.
|
||||||
|
|
||||||
|
* Split former archive page which served as index.html into "index" and
|
||||||
|
"archive", each with their own template, respectively. Index is the landing
|
||||||
|
page and shows by default only the latest 10 articles. Archive shows the full
|
||||||
|
list of articles.
|
||||||
|
|
||||||
|
If you used custom templates,
|
||||||
|
* you should create an "index.html"-template (take blag's default one as a
|
||||||
|
starting point)
|
||||||
|
* you may want to include the new "/archive.html" link somewhere in your
|
||||||
|
navigation
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
* blag comes now with a simple yet good looking default theme that supports
|
||||||
|
syntax highlighting and a light- and dark theme.
|
||||||
|
|
||||||
|
* apart from the generated configuration, `blag quickstart` will now also
|
||||||
|
create the initial directory structure, with the default template, the static
|
||||||
|
directory with the CSS files and the content directory with some initial
|
||||||
|
content to get the user started
|
||||||
|
|
||||||
|
* Added a make target to update the pygments themes
|
||||||
|
|
||||||
|
* updated dependencies:
|
||||||
|
* markdown 3.4.3
|
||||||
|
* pygments 2.15.1
|
||||||
|
* pytest 7.3.2
|
||||||
|
* types-markdown 3.4.2.9
|
||||||
|
* build 0.10.0
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* fixed pyproject.toml to include tests/conftest.py
|
||||||
|
|
||||||
|
|
||||||
## [1.5.0] - 2023-04-16
|
## [1.5.0] - 2023-04-16
|
||||||
|
|
||||||
|
|||||||
5
Makefile
5
Makefile
@@ -48,6 +48,11 @@ test-release: $(VENV) build
|
|||||||
release: $(VENV) build
|
release: $(VENV) build
|
||||||
$(BIN)/twine upload dist/*
|
$(BIN)/twine upload dist/*
|
||||||
|
|
||||||
|
.PHONY: update-pygmentize
|
||||||
|
update-pygmentize: $(VENV)
|
||||||
|
$(BIN)/pygmentize -f html -S default > blag/static/code-light.css
|
||||||
|
$(BIN)/pygmentize -f html -S monokai > blag/static/code-dark.css
|
||||||
|
|
||||||
.PHONY: docs
|
.PHONY: docs
|
||||||
docs: $(VENV)
|
docs: $(VENV)
|
||||||
$(BIN)/sphinx-build $(DOCS_SRC) $(DOCS_OUT)
|
$(BIN)/sphinx-build $(DOCS_SRC) $(DOCS_OUT)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ blag is named after [the blag of the webcomic xkcd][blagxkcd].
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Write content in [Markdown][]
|
* Write content in [Markdown][]
|
||||||
|
* Good looking default theme
|
||||||
* Theming support using [Jinja2][] templates
|
* Theming support using [Jinja2][] templates
|
||||||
* Generation of Atom feeds for blog content
|
* Generation of Atom feeds for blog content
|
||||||
* Fenced code blocks and syntax highlighting using [Pygments][]
|
* Fenced code blocks and syntax highlighting using [Pygments][]
|
||||||
|
|||||||
251
blag/blag.py
251
blag/blag.py
@@ -6,32 +6,28 @@
|
|||||||
|
|
||||||
# remove when we don't support py38 anymore
|
# remove when we don't support py38 anymore
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Any
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import configparser
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import logging
|
|
||||||
import configparser
|
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from jinja2 import (
|
|
||||||
Environment,
|
|
||||||
FileSystemLoader,
|
|
||||||
Template,
|
|
||||||
TemplateNotFound,
|
|
||||||
)
|
|
||||||
import feedgenerator
|
import feedgenerator
|
||||||
|
from jinja2 import Environment, FileSystemLoader, Template, TemplateNotFound
|
||||||
|
|
||||||
import blag
|
import blag
|
||||||
from blag.markdown import markdown_factory, convert_markdown
|
|
||||||
from blag.devserver import serve
|
from blag.devserver import serve
|
||||||
from blag.version import __VERSION__
|
from blag.markdown import convert_markdown, markdown_factory
|
||||||
from blag.quickstart import quickstart
|
from blag.quickstart import quickstart
|
||||||
|
from blag.version import __VERSION__
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format='%(asctime)s %(levelname)s %(name)s %(message)s',
|
format="%(asctime)s %(levelname)s %(name)s %(message)s",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -70,84 +66,84 @@ def parse_args(args: list[str] | None = None) -> argparse.Namespace:
|
|||||||
"""
|
"""
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--version',
|
"--version",
|
||||||
action='version',
|
action="version",
|
||||||
version='%(prog)s ' + __VERSION__,
|
version="%(prog)s " + __VERSION__,
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-v',
|
"-v",
|
||||||
'--verbose',
|
"--verbose",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
help='Verbose output.',
|
help="Verbose output.",
|
||||||
)
|
)
|
||||||
|
|
||||||
commands = parser.add_subparsers(dest='command')
|
commands = parser.add_subparsers(dest="command")
|
||||||
commands.required = True
|
commands.required = True
|
||||||
|
|
||||||
build_parser = commands.add_parser(
|
build_parser = commands.add_parser(
|
||||||
'build',
|
"build",
|
||||||
help='Build website.',
|
help="Build website.",
|
||||||
)
|
)
|
||||||
build_parser.set_defaults(func=build)
|
build_parser.set_defaults(func=build)
|
||||||
build_parser.add_argument(
|
build_parser.add_argument(
|
||||||
'-i',
|
"-i",
|
||||||
'--input-dir',
|
"--input-dir",
|
||||||
default='content',
|
default="content",
|
||||||
help='Input directory (default: content)',
|
help="Input directory (default: content)",
|
||||||
)
|
)
|
||||||
build_parser.add_argument(
|
build_parser.add_argument(
|
||||||
'-o',
|
"-o",
|
||||||
'--output-dir',
|
"--output-dir",
|
||||||
default='build',
|
default="build",
|
||||||
help='Ouptut directory (default: build)',
|
help="Ouptut directory (default: build)",
|
||||||
)
|
)
|
||||||
build_parser.add_argument(
|
build_parser.add_argument(
|
||||||
'-t',
|
"-t",
|
||||||
'--template-dir',
|
"--template-dir",
|
||||||
default='templates',
|
default="templates",
|
||||||
help='Template directory (default: templates)',
|
help="Template directory (default: templates)",
|
||||||
)
|
)
|
||||||
build_parser.add_argument(
|
build_parser.add_argument(
|
||||||
'-s',
|
"-s",
|
||||||
'--static-dir',
|
"--static-dir",
|
||||||
default='static',
|
default="static",
|
||||||
help='Static directory (default: static)',
|
help="Static directory (default: static)",
|
||||||
)
|
)
|
||||||
|
|
||||||
quickstart_parser = commands.add_parser(
|
quickstart_parser = commands.add_parser(
|
||||||
'quickstart',
|
"quickstart",
|
||||||
help="Quickstart blag, creating necessary configuration.",
|
help="Quickstart blag, creating necessary configuration.",
|
||||||
)
|
)
|
||||||
quickstart_parser.set_defaults(func=quickstart)
|
quickstart_parser.set_defaults(func=quickstart)
|
||||||
|
|
||||||
serve_parser = commands.add_parser(
|
serve_parser = commands.add_parser(
|
||||||
'serve',
|
"serve",
|
||||||
help="Start development server.",
|
help="Start development server.",
|
||||||
)
|
)
|
||||||
serve_parser.set_defaults(func=serve)
|
serve_parser.set_defaults(func=serve)
|
||||||
serve_parser.add_argument(
|
serve_parser.add_argument(
|
||||||
'-i',
|
"-i",
|
||||||
'--input-dir',
|
"--input-dir",
|
||||||
default='content',
|
default="content",
|
||||||
help='Input directory (default: content)',
|
help="Input directory (default: content)",
|
||||||
)
|
)
|
||||||
serve_parser.add_argument(
|
serve_parser.add_argument(
|
||||||
'-o',
|
"-o",
|
||||||
'--output-dir',
|
"--output-dir",
|
||||||
default='build',
|
default="build",
|
||||||
help='Ouptut directory (default: build)',
|
help="Ouptut directory (default: build)",
|
||||||
)
|
)
|
||||||
serve_parser.add_argument(
|
serve_parser.add_argument(
|
||||||
'-t',
|
"-t",
|
||||||
'--template-dir',
|
"--template-dir",
|
||||||
default='templates',
|
default="templates",
|
||||||
help='Template directory (default: templates)',
|
help="Template directory (default: templates)",
|
||||||
)
|
)
|
||||||
serve_parser.add_argument(
|
serve_parser.add_argument(
|
||||||
'-s',
|
"-s",
|
||||||
'--static-dir',
|
"--static-dir",
|
||||||
default='static',
|
default="static",
|
||||||
help='Static directory (default: static)',
|
help="Static directory (default: static)",
|
||||||
)
|
)
|
||||||
|
|
||||||
return parser.parse_args(args)
|
return parser.parse_args(args)
|
||||||
@@ -170,18 +166,18 @@ def get_config(configfile: str) -> configparser.SectionProxy:
|
|||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
config.read(configfile)
|
config.read(configfile)
|
||||||
# check for the mandatory options
|
# check for the mandatory options
|
||||||
for value in 'base_url', 'title', 'description', 'author':
|
for value in "base_url", "title", "description", "author":
|
||||||
try:
|
try:
|
||||||
config['main'][value]
|
config["main"][value]
|
||||||
except Exception:
|
except Exception:
|
||||||
print(f'{value} is missing in {configfile}!')
|
print(f"{value} is missing in {configfile}!")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if not config['main']['base_url'].endswith('/'):
|
if not config["main"]["base_url"].endswith("/"):
|
||||||
logger.warning('base_url does not end with a slash, adding it.')
|
logger.warning("base_url does not end with a slash, adding it.")
|
||||||
config['main']['base_url'] += '/'
|
config["main"]["base_url"] += "/"
|
||||||
|
|
||||||
return config['main']
|
return config["main"]
|
||||||
|
|
||||||
|
|
||||||
def environment_factory(
|
def environment_factory(
|
||||||
@@ -222,50 +218,51 @@ def build(args: argparse.Namespace) -> None:
|
|||||||
args
|
args
|
||||||
|
|
||||||
"""
|
"""
|
||||||
os.makedirs(f'{args.output_dir}', exist_ok=True)
|
os.makedirs(f"{args.output_dir}", exist_ok=True)
|
||||||
convertibles = []
|
convertibles = []
|
||||||
for root, dirnames, filenames in os.walk(args.input_dir):
|
for root, dirnames, filenames in os.walk(args.input_dir):
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
rel_src = os.path.relpath(
|
rel_src = os.path.relpath(
|
||||||
f'{root}/{filename}', start=args.input_dir
|
f"{root}/{filename}", start=args.input_dir
|
||||||
)
|
)
|
||||||
# all non-markdown files are just copied over, the markdown
|
# all non-markdown files are just copied over, the markdown
|
||||||
# files are converted to html
|
# files are converted to html
|
||||||
if rel_src.endswith('.md'):
|
if rel_src.endswith(".md"):
|
||||||
rel_dst = rel_src
|
rel_dst = rel_src
|
||||||
rel_dst = rel_dst[:-3] + '.html'
|
rel_dst = rel_dst[:-3] + ".html"
|
||||||
convertibles.append((rel_src, rel_dst))
|
convertibles.append((rel_src, rel_dst))
|
||||||
else:
|
else:
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
f'{args.input_dir}/{rel_src}',
|
f"{args.input_dir}/{rel_src}",
|
||||||
f'{args.output_dir}/{rel_src}',
|
f"{args.output_dir}/{rel_src}",
|
||||||
)
|
)
|
||||||
for dirname in dirnames:
|
for dirname in dirnames:
|
||||||
# all directories are copied into the output directory
|
# all directories are copied into the output directory
|
||||||
path = os.path.relpath(f'{root}/{dirname}', start=args.input_dir)
|
path = os.path.relpath(f"{root}/{dirname}", start=args.input_dir)
|
||||||
os.makedirs(f'{args.output_dir}/{path}', exist_ok=True)
|
os.makedirs(f"{args.output_dir}/{path}", exist_ok=True)
|
||||||
|
|
||||||
# copy static files over
|
# copy static files over
|
||||||
logger.info('Copying static files.')
|
logger.info("Copying static files.")
|
||||||
if os.path.exists(args.static_dir):
|
if os.path.exists(args.static_dir):
|
||||||
shutil.copytree(args.static_dir, args.output_dir, dirs_exist_ok=True)
|
shutil.copytree(args.static_dir, args.output_dir, dirs_exist_ok=True)
|
||||||
|
|
||||||
config = get_config('config.ini')
|
config = get_config("config.ini")
|
||||||
|
|
||||||
env = environment_factory(args.template_dir, dict(site=config))
|
env = environment_factory(args.template_dir, dict(site=config))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
page_template = env.get_template('page.html')
|
page_template = env.get_template("page.html")
|
||||||
article_template = env.get_template('article.html')
|
article_template = env.get_template("article.html")
|
||||||
archive_template = env.get_template('archive.html')
|
index_template = env.get_template("index.html")
|
||||||
tags_template = env.get_template('tags.html')
|
archive_template = env.get_template("archive.html")
|
||||||
tag_template = env.get_template('tag.html')
|
tags_template = env.get_template("tags.html")
|
||||||
|
tag_template = env.get_template("tag.html")
|
||||||
except TemplateNotFound as exc:
|
except TemplateNotFound as exc:
|
||||||
tmpl = os.path.join(blag.__path__[0], 'templates')
|
tmpl = os.path.join(blag.__path__[0], "templates")
|
||||||
logger.error(
|
logger.error(
|
||||||
f'Template "{exc.name}" not found in {args.template_dir}! '
|
f'Template "{exc.name}" not found in {args.template_dir}! '
|
||||||
'Consider running `blag quickstart` or copying the '
|
"Consider running `blag quickstart` or copying the "
|
||||||
f'missing template from {tmpl}.'
|
f"missing template from {tmpl}."
|
||||||
)
|
)
|
||||||
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -281,11 +278,12 @@ def build(args: argparse.Namespace) -> None:
|
|||||||
generate_feed(
|
generate_feed(
|
||||||
articles,
|
articles,
|
||||||
args.output_dir,
|
args.output_dir,
|
||||||
base_url=config['base_url'],
|
base_url=config["base_url"],
|
||||||
blog_title=config['title'],
|
blog_title=config["title"],
|
||||||
blog_description=config['description'],
|
blog_description=config["description"],
|
||||||
blog_author=config['author'],
|
blog_author=config["author"],
|
||||||
)
|
)
|
||||||
|
generate_index(articles, index_template, args.output_dir)
|
||||||
generate_archive(articles, archive_template, args.output_dir)
|
generate_archive(articles, archive_template, args.output_dir)
|
||||||
generate_tags(articles, tags_template, tag_template, args.output_dir)
|
generate_tags(articles, tags_template, tag_template, args.output_dir)
|
||||||
|
|
||||||
@@ -305,6 +303,8 @@ def process_markdown(
|
|||||||
If a markdown file has a `date` metadata field it will be recognized
|
If a markdown file has a `date` metadata field it will be recognized
|
||||||
as article otherwise as page.
|
as article otherwise as page.
|
||||||
|
|
||||||
|
Articles are sorted by date in descending order.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
convertibles
|
convertibles
|
||||||
@@ -317,7 +317,7 @@ def process_markdown(
|
|||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
list[tuple[str, dict[str, Any]]], list[tuple[str, dict[str, Any]]]
|
list[tuple[str, dict[str, Any]]], list[tuple[str, dict[str, Any]]]
|
||||||
articles and pages
|
articles and pages, articles are sorted by date in descending order.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
logger.info("Converting Markdown files...")
|
logger.info("Converting Markdown files...")
|
||||||
@@ -326,9 +326,9 @@ def process_markdown(
|
|||||||
articles = []
|
articles = []
|
||||||
pages = []
|
pages = []
|
||||||
for src, dst in convertibles:
|
for src, dst in convertibles:
|
||||||
logger.debug(f'Processing {src}')
|
logger.debug(f"Processing {src}")
|
||||||
|
|
||||||
with open(f'{input_dir}/{src}', 'r') as fh:
|
with open(f"{input_dir}/{src}", "r") as fh:
|
||||||
body = fh.read()
|
body = fh.read()
|
||||||
|
|
||||||
content, meta = convert_markdown(md, body)
|
content, meta = convert_markdown(md, body)
|
||||||
@@ -338,17 +338,17 @@ def process_markdown(
|
|||||||
|
|
||||||
# if markdown has date in meta, we treat it as a blog article,
|
# if markdown has date in meta, we treat it as a blog article,
|
||||||
# everything else are just pages
|
# everything else are just pages
|
||||||
if meta and 'date' in meta:
|
if meta and "date" in meta:
|
||||||
articles.append((dst, context))
|
articles.append((dst, context))
|
||||||
result = article_template.render(context)
|
result = article_template.render(context)
|
||||||
else:
|
else:
|
||||||
pages.append((dst, context))
|
pages.append((dst, context))
|
||||||
result = page_template.render(context)
|
result = page_template.render(context)
|
||||||
with open(f'{output_dir}/{dst}', 'w') as fh_dest:
|
with open(f"{output_dir}/{dst}", "w") as fh_dest:
|
||||||
fh_dest.write(result)
|
fh_dest.write(result)
|
||||||
|
|
||||||
# sort articles by date, descending
|
# sort articles by date, descending
|
||||||
articles = sorted(articles, key=lambda x: x[1]['date'], reverse=True)
|
articles = sorted(articles, key=lambda x: x[1]["date"], reverse=True)
|
||||||
return articles, pages
|
return articles, pages
|
||||||
|
|
||||||
|
|
||||||
@@ -378,38 +378,40 @@ def generate_feed(
|
|||||||
blog author
|
blog author
|
||||||
|
|
||||||
"""
|
"""
|
||||||
logger.info('Generating Atom feed.')
|
logger.info("Generating Atom feed.")
|
||||||
feed = feedgenerator.Atom1Feed(
|
feed = feedgenerator.Atom1Feed(
|
||||||
link=base_url,
|
link=base_url,
|
||||||
title=blog_title,
|
title=blog_title,
|
||||||
description=blog_description,
|
description=blog_description,
|
||||||
feed_url=base_url + 'atom.xml',
|
feed_url=base_url + "atom.xml",
|
||||||
)
|
)
|
||||||
|
|
||||||
for dst, context in articles:
|
for dst, context in articles:
|
||||||
# if article has a description, use that. otherwise fall back to
|
# if article has a description, use that. otherwise fall back to
|
||||||
# the title
|
# the title
|
||||||
description = context.get('description', context['title'])
|
description = context.get("description", context["title"])
|
||||||
|
|
||||||
feed.add_item(
|
feed.add_item(
|
||||||
title=context['title'],
|
title=context["title"],
|
||||||
author_name=blog_author,
|
author_name=blog_author,
|
||||||
link=base_url + dst,
|
link=base_url + dst,
|
||||||
description=description,
|
description=description,
|
||||||
content=context['content'],
|
content=context["content"],
|
||||||
pubdate=context['date'],
|
pubdate=context["date"],
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(f'{output_dir}/atom.xml', 'w') as fh:
|
with open(f"{output_dir}/atom.xml", "w") as fh:
|
||||||
feed.write(fh, encoding='utf8')
|
feed.write(fh, encoding="utf8")
|
||||||
|
|
||||||
|
|
||||||
def generate_archive(
|
def generate_index(
|
||||||
articles: list[tuple[str, dict[str, Any]]],
|
articles: list[tuple[str, dict[str, Any]]],
|
||||||
template: Template,
|
template: Template,
|
||||||
output_dir: str,
|
output_dir: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Generate the archive page.
|
"""Generate the index page.
|
||||||
|
|
||||||
|
This is used for the index (i.e. landing) page.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
@@ -423,11 +425,40 @@ def generate_archive(
|
|||||||
archive = []
|
archive = []
|
||||||
for dst, context in articles:
|
for dst, context in articles:
|
||||||
entry = context.copy()
|
entry = context.copy()
|
||||||
entry['dst'] = dst
|
entry["dst"] = dst
|
||||||
archive.append(entry)
|
archive.append(entry)
|
||||||
|
|
||||||
result = template.render(dict(archive=archive))
|
result = template.render(dict(archive=archive))
|
||||||
with open(f'{output_dir}/index.html', 'w') as fh:
|
with open(f"{output_dir}/index.html", "w") as fh:
|
||||||
|
fh.write(result)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_archive(
|
||||||
|
articles: list[tuple[str, dict[str, Any]]],
|
||||||
|
template: Template,
|
||||||
|
output_dir: str,
|
||||||
|
) -> None:
|
||||||
|
"""Generate the archive page.
|
||||||
|
|
||||||
|
This is used for the full archive.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
articles
|
||||||
|
List of articles. Each article has the destination path and a
|
||||||
|
dictionary with the content.
|
||||||
|
template
|
||||||
|
output_dir
|
||||||
|
|
||||||
|
"""
|
||||||
|
archive = []
|
||||||
|
for dst, context in articles:
|
||||||
|
entry = context.copy()
|
||||||
|
entry["dst"] = dst
|
||||||
|
archive.append(entry)
|
||||||
|
|
||||||
|
result = template.render(dict(archive=archive))
|
||||||
|
with open(f"{output_dir}/archive.html", "w") as fh:
|
||||||
fh.write(result)
|
fh.write(result)
|
||||||
|
|
||||||
|
|
||||||
@@ -449,11 +480,11 @@ def generate_tags(
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
logger.info("Generating Tag-pages.")
|
logger.info("Generating Tag-pages.")
|
||||||
os.makedirs(f'{output_dir}/tags', exist_ok=True)
|
os.makedirs(f"{output_dir}/tags", exist_ok=True)
|
||||||
# get tags number of occurrences
|
# get tags number of occurrences
|
||||||
all_tags: dict[str, int] = {}
|
all_tags: dict[str, int] = {}
|
||||||
for _, context in articles:
|
for _, context in articles:
|
||||||
tags: list[str] = context.get('tags', [])
|
tags: list[str] = context.get("tags", [])
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
all_tags[tag] = all_tags.get(tag, 0) + 1
|
all_tags[tag] = all_tags.get(tag, 0) + 1
|
||||||
# sort by occurrence
|
# sort by occurrence
|
||||||
@@ -462,25 +493,25 @@ def generate_tags(
|
|||||||
)
|
)
|
||||||
|
|
||||||
result = tags_template.render(dict(tags=taglist))
|
result = tags_template.render(dict(tags=taglist))
|
||||||
with open(f'{output_dir}/tags/index.html', 'w') as fh:
|
with open(f"{output_dir}/tags/index.html", "w") as fh:
|
||||||
fh.write(result)
|
fh.write(result)
|
||||||
|
|
||||||
# get tags and archive per tag
|
# get tags and archive per tag
|
||||||
all_tags2: dict[str, list[dict[str, Any]]] = {}
|
all_tags2: dict[str, list[dict[str, Any]]] = {}
|
||||||
for dst, context in articles:
|
for dst, context in articles:
|
||||||
tags = context.get('tags', [])
|
tags = context.get("tags", [])
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
archive: list[dict[str, Any]] = all_tags2.get(tag, [])
|
archive: list[dict[str, Any]] = all_tags2.get(tag, [])
|
||||||
entry = context.copy()
|
entry = context.copy()
|
||||||
entry['dst'] = dst
|
entry["dst"] = dst
|
||||||
archive.append(entry)
|
archive.append(entry)
|
||||||
all_tags2[tag] = archive
|
all_tags2[tag] = archive
|
||||||
|
|
||||||
for tag, archive in all_tags2.items():
|
for tag, archive in all_tags2.items():
|
||||||
result = tag_template.render(dict(archive=archive, tag=tag))
|
result = tag_template.render(dict(archive=archive, tag=tag))
|
||||||
with open(f'{output_dir}/tags/{tag}.html', 'w') as fh:
|
with open(f"{output_dir}/tags/{tag}.html", "w") as fh:
|
||||||
fh.write(result)
|
fh.write(result)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
8
blag/content/about.md
Normal file
8
blag/content/about.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
title: About Me
|
||||||
|
description: Short description of this page.
|
||||||
|
|
||||||
|
|
||||||
|
## About Me
|
||||||
|
|
||||||
|
This is a regular page, i.e. not a blog post. Feel free to delete this page,
|
||||||
|
populate it with more content or generate more [pages like this](testpage.md).
|
||||||
51
blag/content/hello-world.md
Normal file
51
blag/content/hello-world.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
Title: Hello World!
|
||||||
|
Description: Hello there, this is the first blog post. You should read me first.
|
||||||
|
Date: 2023-01-01 12:00
|
||||||
|
Tags: blag, pygments
|
||||||
|
|
||||||
|
|
||||||
|
## Hello World
|
||||||
|
|
||||||
|
This is an example blog post. Internally, blag differentiates between **pages**
|
||||||
|
and **articles**. Intuitively, pages are simple pages and articles are blog
|
||||||
|
posts. The decision whether a document is a page or an article is made
|
||||||
|
depending on the presence of the `date` metadata element: Any document that
|
||||||
|
contains the `date` metadata element is an article, everything else a page.
|
||||||
|
|
||||||
|
This differentiation has consequences:
|
||||||
|
|
||||||
|
* blag uses different templates: `page.html` and `article.html`
|
||||||
|
* only articles are collected in the Atom feed
|
||||||
|
* only articles are aggregated in the tag pages
|
||||||
|
|
||||||
|
For more detailed information, please refer to the [documentation][doc]
|
||||||
|
|
||||||
|
[doc]: https://blag.readthedocs.io
|
||||||
|
|
||||||
|
|
||||||
|
### Syntax Highlighting
|
||||||
|
|
||||||
|
```python
|
||||||
|
def foo(bar):
|
||||||
|
"""This is a docstring.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# comment
|
||||||
|
return bar
|
||||||
|
```
|
||||||
|
|
||||||
|
Syntax highlighting is done via [Pygments][pygments]. For code blocks, blag
|
||||||
|
generates the necessary CSS classes by default, which you can use to style your
|
||||||
|
code using CSS. It provides you with a default light- and dark theme, for more
|
||||||
|
information on how to generate a different theme, please refer to [Pygments'
|
||||||
|
documentation][pygments].
|
||||||
|
|
||||||
|
[pygments]: https://pygments.org
|
||||||
|
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
* Adapt the files in `templates` to your needs
|
||||||
|
* Check out the files in `static` and modify as needed
|
||||||
|
* Add some content
|
||||||
|
* Change the [favicon.ico](favicon.ico)
|
||||||
9
blag/content/second-post.md
Normal file
9
blag/content/second-post.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Title: Second Post
|
||||||
|
Description: This is the second blog post, so you can see how it looks like on the front page.
|
||||||
|
Date: 2023-01-02 12:00
|
||||||
|
Tags: blag
|
||||||
|
|
||||||
|
|
||||||
|
## Second Post
|
||||||
|
|
||||||
|
This page serves no purpose :)
|
||||||
46
blag/content/testpage.md
Normal file
46
blag/content/testpage.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# This Is A Headline
|
||||||
|
|
||||||
|
This is some **bold text** with some `code` inside. This is _some_underlined_
|
||||||
|
text with some `code` inside. This is some text with some `code` inside. This
|
||||||
|
is some text with some `code` inside. This is some text with some `code`
|
||||||
|
inside. This is some text with some `code` inside. This is some text with some
|
||||||
|
`code` inside. This is some text with some `code` inside.
|
||||||
|
|
||||||
|
This is some [link](https://example.com) inside the text -- it does not really
|
||||||
|
lead anywhere! This is some [link](https://example.com) inside the text -- it
|
||||||
|
does not really lead anywhere! This is some [link](https://example.com) inside
|
||||||
|
the text -- it does not really lead anywhere!
|
||||||
|
|
||||||
|
|
||||||
|
* some bullets
|
||||||
|
* some other
|
||||||
|
* bullets
|
||||||
|
* foo
|
||||||
|
|
||||||
|
```python
|
||||||
|
# this is some python code
|
||||||
|
|
||||||
|
class Foo:
|
||||||
|
|
||||||
|
def __init__(self, foo, bar):
|
||||||
|
self.foo = foo
|
||||||
|
self.bar = bar
|
||||||
|
|
||||||
|
def do_something():
|
||||||
|
"""This is the docstring of this method.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return foo
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Some other headline
|
||||||
|
|
||||||
|
This is some other text
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
|
||||||
|
# some comment
|
||||||
|
foo:
|
||||||
|
ls -lh
|
||||||
|
```
|
||||||
@@ -8,18 +8,18 @@ site if necessary.
|
|||||||
|
|
||||||
# remove when we don't support py38 anymore
|
# remove when we don't support py38 anymore
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import NoReturn
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
import multiprocessing
|
|
||||||
from http.server import SimpleHTTPRequestHandler, HTTPServer
|
|
||||||
from functools import partial
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import logging
|
||||||
|
import multiprocessing
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from functools import partial
|
||||||
|
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||||
|
from typing import NoReturn
|
||||||
|
|
||||||
from blag import blag
|
from blag import blag
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ def autoreload(args: argparse.Namespace) -> NoReturn:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
dirs = [args.input_dir, args.template_dir, args.static_dir]
|
dirs = [args.input_dir, args.template_dir, args.static_dir]
|
||||||
logger.info(f'Monitoring {dirs} for changes...')
|
logger.info(f"Monitoring {dirs} for changes...")
|
||||||
# make sure we trigger the rebuild immediately when we enter the
|
# make sure we trigger the rebuild immediately when we enter the
|
||||||
# loop to avoid serving stale contents
|
# loop to avoid serving stale contents
|
||||||
last_mtime = 0.0
|
last_mtime = 0.0
|
||||||
@@ -77,7 +77,7 @@ def autoreload(args: argparse.Namespace) -> NoReturn:
|
|||||||
mtime = get_last_modified(dirs)
|
mtime = get_last_modified(dirs)
|
||||||
if mtime > last_mtime:
|
if mtime > last_mtime:
|
||||||
last_mtime = mtime
|
last_mtime = mtime
|
||||||
logger.info('Change detected, rebuilding...')
|
logger.info("Change detected, rebuilding...")
|
||||||
blag.build(args)
|
blag.build(args)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ def serve(args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
httpd = HTTPServer(
|
httpd = HTTPServer(
|
||||||
('', 8000),
|
("", 8000),
|
||||||
partial(SimpleHTTPRequestHandler, directory=args.output_dir),
|
partial(SimpleHTTPRequestHandler, directory=args.output_dir),
|
||||||
)
|
)
|
||||||
proc = multiprocessing.Process(target=autoreload, args=(args,))
|
proc = multiprocessing.Process(target=autoreload, args=(args,))
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ processing.
|
|||||||
|
|
||||||
# remove when we don't support py38 anymore
|
# remove when we don't support py38 anymore
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from datetime import datetime
|
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
from urllib.parse import urlsplit, urlunsplit
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
from xml.etree.ElementTree import Element
|
from xml.etree.ElementTree import Element
|
||||||
|
|
||||||
@@ -16,7 +17,6 @@ from markdown import Markdown
|
|||||||
from markdown.extensions import Extension
|
from markdown.extensions import Extension
|
||||||
from markdown.treeprocessors import Treeprocessor
|
from markdown.treeprocessors import Treeprocessor
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -33,13 +33,13 @@ def markdown_factory() -> Markdown:
|
|||||||
"""
|
"""
|
||||||
md = Markdown(
|
md = Markdown(
|
||||||
extensions=[
|
extensions=[
|
||||||
'meta',
|
"meta",
|
||||||
'fenced_code',
|
"fenced_code",
|
||||||
'codehilite',
|
"codehilite",
|
||||||
'smarty',
|
"smarty",
|
||||||
MarkdownLinkExtension(),
|
MarkdownLinkExtension(),
|
||||||
],
|
],
|
||||||
output_format='html',
|
output_format="html",
|
||||||
)
|
)
|
||||||
return md
|
return md
|
||||||
|
|
||||||
@@ -75,20 +75,20 @@ def convert_markdown(
|
|||||||
# markdowns metadata consists as list of strings -- one item per
|
# markdowns metadata consists as list of strings -- one item per
|
||||||
# line. let's convert into single strings.
|
# line. let's convert into single strings.
|
||||||
for key, value in meta.items():
|
for key, value in meta.items():
|
||||||
value = '\n'.join(value)
|
value = "\n".join(value)
|
||||||
meta[key] = value
|
meta[key] = value
|
||||||
|
|
||||||
# convert known metadata
|
# convert known metadata
|
||||||
# date: datetime
|
# date: datetime
|
||||||
if 'date' in meta:
|
if "date" in meta:
|
||||||
meta['date'] = datetime.fromisoformat(meta['date'])
|
meta["date"] = datetime.fromisoformat(meta["date"])
|
||||||
meta['date'] = meta['date'].astimezone()
|
meta["date"] = meta["date"].astimezone()
|
||||||
# tags: list[str] and lower case
|
# tags: list[str] and lower case
|
||||||
if 'tags' in meta:
|
if "tags" in meta:
|
||||||
tags = meta['tags'].split(',')
|
tags = meta["tags"].split(",")
|
||||||
tags = [t.lower() for t in tags]
|
tags = [t.lower() for t in tags]
|
||||||
tags = [t.strip() for t in tags]
|
tags = [t.strip() for t in tags]
|
||||||
meta['tags'] = tags
|
meta["tags"] = tags
|
||||||
|
|
||||||
return content, meta
|
return content, meta
|
||||||
|
|
||||||
@@ -98,25 +98,25 @@ class MarkdownLinkTreeprocessor(Treeprocessor):
|
|||||||
|
|
||||||
def run(self, root: Element) -> Element:
|
def run(self, root: Element) -> Element:
|
||||||
for element in root.iter():
|
for element in root.iter():
|
||||||
if element.tag == 'a':
|
if element.tag == "a":
|
||||||
url = element.get('href')
|
url = element.get("href")
|
||||||
# element.get could also return None, we haven't seen this so
|
# element.get could also return None, we haven't seen this so
|
||||||
# far, so lets wait if we raise this
|
# far, so lets wait if we raise this
|
||||||
assert url is not None
|
assert url is not None
|
||||||
url = str(url)
|
url = str(url)
|
||||||
converted = self.convert(url)
|
converted = self.convert(url)
|
||||||
element.set('href', converted)
|
element.set("href", converted)
|
||||||
return root
|
return root
|
||||||
|
|
||||||
def convert(self, url: str) -> str:
|
def convert(self, url: str) -> str:
|
||||||
scheme, netloc, path, query, fragment = urlsplit(url)
|
scheme, netloc, path, query, fragment = urlsplit(url)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'{url}: {scheme=} {netloc=} {path=} {query=} {fragment=}'
|
f"{url}: {scheme=} {netloc=} {path=} {query=} {fragment=}"
|
||||||
)
|
)
|
||||||
if scheme or netloc or not path:
|
if scheme or netloc or not path:
|
||||||
return url
|
return url
|
||||||
if path.endswith('.md'):
|
if path.endswith(".md"):
|
||||||
path = path[:-3] + '.html'
|
path = path[:-3] + ".html"
|
||||||
|
|
||||||
url = urlunsplit((scheme, netloc, path, query, fragment))
|
url = urlunsplit((scheme, netloc, path, query, fragment))
|
||||||
return url
|
return url
|
||||||
@@ -128,6 +128,6 @@ class MarkdownLinkExtension(Extension):
|
|||||||
def extendMarkdown(self, md: Markdown) -> None:
|
def extendMarkdown(self, md: Markdown) -> None:
|
||||||
md.treeprocessors.register(
|
md.treeprocessors.register(
|
||||||
MarkdownLinkTreeprocessor(md),
|
MarkdownLinkTreeprocessor(md),
|
||||||
'mdlink',
|
"mdlink",
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
|
|
||||||
# remove when we don't support py38 anymore
|
# remove when we don't support py38 anymore
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import configparser
|
|
||||||
import argparse
|
import argparse
|
||||||
import shutil
|
import configparser
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
import blag
|
import blag
|
||||||
|
|
||||||
@@ -37,27 +38,33 @@ def get_input(question: str, default: str) -> str:
|
|||||||
return reply
|
return reply
|
||||||
|
|
||||||
|
|
||||||
def copy_templates() -> None:
|
def copy_default_theme() -> None:
|
||||||
"""Copy templates into current directory.
|
"""Copy default theme into current directory.
|
||||||
|
|
||||||
|
The default theme contains the 'templates', 'content' and 'static'
|
||||||
|
directories shipped with blag.
|
||||||
|
|
||||||
It will not overwrite existing files.
|
It will not overwrite existing files.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
print("Copying templates...")
|
print("Copying default theme...")
|
||||||
|
for dir_ in "templates", "content", "static":
|
||||||
|
print(f" Copying {dir_}...")
|
||||||
try:
|
try:
|
||||||
shutil.copytree(
|
shutil.copytree(
|
||||||
os.path.join(blag.__path__[0], 'templates'),
|
os.path.join(blag.__path__[0], dir_),
|
||||||
'templates',
|
dir_,
|
||||||
)
|
)
|
||||||
except FileExistsError:
|
except FileExistsError:
|
||||||
print("Templates already exist. Skipping.")
|
print(f" {dir_} already exist. Skipping.")
|
||||||
|
|
||||||
|
|
||||||
def quickstart(args: argparse.Namespace | None) -> None:
|
def quickstart(args: argparse.Namespace | None) -> None:
|
||||||
"""Quickstart.
|
"""Quickstart.
|
||||||
|
|
||||||
This method asks the user some questions and generates a
|
This method asks the user some questions and generates a configuration file
|
||||||
configuration file that is needed in order to run blag.
|
that is needed in order to run blag. Additionally, it creates the content
|
||||||
|
and static directories with some initial content, to get the user started.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
@@ -83,13 +90,13 @@ def quickstart(args: argparse.Namespace | None) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
config['main'] = {
|
config["main"] = {
|
||||||
'base_url': base_url,
|
"base_url": base_url,
|
||||||
'title': title,
|
"title": title,
|
||||||
'description': description,
|
"description": description,
|
||||||
'author': author,
|
"author": author,
|
||||||
}
|
}
|
||||||
with open('config.ini', 'w') as fh:
|
with open("config.ini", "w") as fh:
|
||||||
config.write(fh)
|
config.write(fh)
|
||||||
|
|
||||||
copy_templates()
|
copy_default_theme()
|
||||||
|
|||||||
83
blag/static/code-dark.css
Normal file
83
blag/static/code-dark.css
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
pre { line-height: 125%; }
|
||||||
|
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||||
|
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||||
|
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||||
|
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||||
|
.hll { background-color: #49483e }
|
||||||
|
.c { color: #75715e } /* Comment */
|
||||||
|
.err { color: #960050; background-color: #1e0010 } /* Error */
|
||||||
|
.esc { color: #f8f8f2 } /* Escape */
|
||||||
|
.g { color: #f8f8f2 } /* Generic */
|
||||||
|
.k { color: #66d9ef } /* Keyword */
|
||||||
|
.l { color: #ae81ff } /* Literal */
|
||||||
|
.n { color: #f8f8f2 } /* Name */
|
||||||
|
.o { color: #f92672 } /* Operator */
|
||||||
|
.x { color: #f8f8f2 } /* Other */
|
||||||
|
.p { color: #f8f8f2 } /* Punctuation */
|
||||||
|
.ch { color: #75715e } /* Comment.Hashbang */
|
||||||
|
.cm { color: #75715e } /* Comment.Multiline */
|
||||||
|
.cp { color: #75715e } /* Comment.Preproc */
|
||||||
|
.cpf { color: #75715e } /* Comment.PreprocFile */
|
||||||
|
.c1 { color: #75715e } /* Comment.Single */
|
||||||
|
.cs { color: #75715e } /* Comment.Special */
|
||||||
|
.gd { color: #f92672 } /* Generic.Deleted */
|
||||||
|
.ge { color: #f8f8f2; font-style: italic } /* Generic.Emph */
|
||||||
|
.gr { color: #f8f8f2 } /* Generic.Error */
|
||||||
|
.gh { color: #f8f8f2 } /* Generic.Heading */
|
||||||
|
.gi { color: #a6e22e } /* Generic.Inserted */
|
||||||
|
.go { color: #66d9ef } /* Generic.Output */
|
||||||
|
.gp { color: #f92672; font-weight: bold } /* Generic.Prompt */
|
||||||
|
.gs { color: #f8f8f2; font-weight: bold } /* Generic.Strong */
|
||||||
|
.gu { color: #75715e } /* Generic.Subheading */
|
||||||
|
.gt { color: #f8f8f2 } /* Generic.Traceback */
|
||||||
|
.kc { color: #66d9ef } /* Keyword.Constant */
|
||||||
|
.kd { color: #66d9ef } /* Keyword.Declaration */
|
||||||
|
.kn { color: #f92672 } /* Keyword.Namespace */
|
||||||
|
.kp { color: #66d9ef } /* Keyword.Pseudo */
|
||||||
|
.kr { color: #66d9ef } /* Keyword.Reserved */
|
||||||
|
.kt { color: #66d9ef } /* Keyword.Type */
|
||||||
|
.ld { color: #e6db74 } /* Literal.Date */
|
||||||
|
.m { color: #ae81ff } /* Literal.Number */
|
||||||
|
.s { color: #e6db74 } /* Literal.String */
|
||||||
|
.na { color: #a6e22e } /* Name.Attribute */
|
||||||
|
.nb { color: #f8f8f2 } /* Name.Builtin */
|
||||||
|
.nc { color: #a6e22e } /* Name.Class */
|
||||||
|
.no { color: #66d9ef } /* Name.Constant */
|
||||||
|
.nd { color: #a6e22e } /* Name.Decorator */
|
||||||
|
.ni { color: #f8f8f2 } /* Name.Entity */
|
||||||
|
.ne { color: #a6e22e } /* Name.Exception */
|
||||||
|
.nf { color: #a6e22e } /* Name.Function */
|
||||||
|
.nl { color: #f8f8f2 } /* Name.Label */
|
||||||
|
.nn { color: #f8f8f2 } /* Name.Namespace */
|
||||||
|
.nx { color: #a6e22e } /* Name.Other */
|
||||||
|
.py { color: #f8f8f2 } /* Name.Property */
|
||||||
|
.nt { color: #f92672 } /* Name.Tag */
|
||||||
|
.nv { color: #f8f8f2 } /* Name.Variable */
|
||||||
|
.ow { color: #f92672 } /* Operator.Word */
|
||||||
|
.pm { color: #f8f8f2 } /* Punctuation.Marker */
|
||||||
|
.w { color: #f8f8f2 } /* Text.Whitespace */
|
||||||
|
.mb { color: #ae81ff } /* Literal.Number.Bin */
|
||||||
|
.mf { color: #ae81ff } /* Literal.Number.Float */
|
||||||
|
.mh { color: #ae81ff } /* Literal.Number.Hex */
|
||||||
|
.mi { color: #ae81ff } /* Literal.Number.Integer */
|
||||||
|
.mo { color: #ae81ff } /* Literal.Number.Oct */
|
||||||
|
.sa { color: #e6db74 } /* Literal.String.Affix */
|
||||||
|
.sb { color: #e6db74 } /* Literal.String.Backtick */
|
||||||
|
.sc { color: #e6db74 } /* Literal.String.Char */
|
||||||
|
.dl { color: #e6db74 } /* Literal.String.Delimiter */
|
||||||
|
.sd { color: #e6db74 } /* Literal.String.Doc */
|
||||||
|
.s2 { color: #e6db74 } /* Literal.String.Double */
|
||||||
|
.se { color: #ae81ff } /* Literal.String.Escape */
|
||||||
|
.sh { color: #e6db74 } /* Literal.String.Heredoc */
|
||||||
|
.si { color: #e6db74 } /* Literal.String.Interpol */
|
||||||
|
.sx { color: #e6db74 } /* Literal.String.Other */
|
||||||
|
.sr { color: #e6db74 } /* Literal.String.Regex */
|
||||||
|
.s1 { color: #e6db74 } /* Literal.String.Single */
|
||||||
|
.ss { color: #e6db74 } /* Literal.String.Symbol */
|
||||||
|
.bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */
|
||||||
|
.fm { color: #a6e22e } /* Name.Function.Magic */
|
||||||
|
.vc { color: #f8f8f2 } /* Name.Variable.Class */
|
||||||
|
.vg { color: #f8f8f2 } /* Name.Variable.Global */
|
||||||
|
.vi { color: #f8f8f2 } /* Name.Variable.Instance */
|
||||||
|
.vm { color: #f8f8f2 } /* Name.Variable.Magic */
|
||||||
|
.il { color: #ae81ff } /* Literal.Number.Integer.Long */
|
||||||
73
blag/static/code-light.css
Normal file
73
blag/static/code-light.css
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
pre { line-height: 125%; }
|
||||||
|
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||||
|
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||||
|
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||||
|
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||||
|
.hll { background-color: #ffffcc }
|
||||||
|
.c { color: #3D7B7B; font-style: italic } /* Comment */
|
||||||
|
.err { border: 1px solid #FF0000 } /* Error */
|
||||||
|
.k { color: #008000; font-weight: bold } /* Keyword */
|
||||||
|
.o { color: #666666 } /* Operator */
|
||||||
|
.ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
|
||||||
|
.cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
|
||||||
|
.cp { color: #9C6500 } /* Comment.Preproc */
|
||||||
|
.cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */
|
||||||
|
.c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */
|
||||||
|
.cs { color: #3D7B7B; font-style: italic } /* Comment.Special */
|
||||||
|
.gd { color: #A00000 } /* Generic.Deleted */
|
||||||
|
.ge { font-style: italic } /* Generic.Emph */
|
||||||
|
.gr { color: #E40000 } /* Generic.Error */
|
||||||
|
.gh { color: #000080; font-weight: bold } /* Generic.Heading */
|
||||||
|
.gi { color: #008400 } /* Generic.Inserted */
|
||||||
|
.go { color: #717171 } /* Generic.Output */
|
||||||
|
.gp { color: #000080; font-weight: bold } /* Generic.Prompt */
|
||||||
|
.gs { font-weight: bold } /* Generic.Strong */
|
||||||
|
.gu { color: #800080; font-weight: bold } /* Generic.Subheading */
|
||||||
|
.gt { color: #0044DD } /* Generic.Traceback */
|
||||||
|
.kc { color: #008000; font-weight: bold } /* Keyword.Constant */
|
||||||
|
.kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
|
||||||
|
.kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
|
||||||
|
.kp { color: #008000 } /* Keyword.Pseudo */
|
||||||
|
.kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
|
||||||
|
.kt { color: #B00040 } /* Keyword.Type */
|
||||||
|
.m { color: #666666 } /* Literal.Number */
|
||||||
|
.s { color: #BA2121 } /* Literal.String */
|
||||||
|
.na { color: #687822 } /* Name.Attribute */
|
||||||
|
.nb { color: #008000 } /* Name.Builtin */
|
||||||
|
.nc { color: #0000FF; font-weight: bold } /* Name.Class */
|
||||||
|
.no { color: #880000 } /* Name.Constant */
|
||||||
|
.nd { color: #AA22FF } /* Name.Decorator */
|
||||||
|
.ni { color: #717171; font-weight: bold } /* Name.Entity */
|
||||||
|
.ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
|
||||||
|
.nf { color: #0000FF } /* Name.Function */
|
||||||
|
.nl { color: #767600 } /* Name.Label */
|
||||||
|
.nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
|
||||||
|
.nt { color: #008000; font-weight: bold } /* Name.Tag */
|
||||||
|
.nv { color: #19177C } /* Name.Variable */
|
||||||
|
.ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
|
||||||
|
.w { color: #bbbbbb } /* Text.Whitespace */
|
||||||
|
.mb { color: #666666 } /* Literal.Number.Bin */
|
||||||
|
.mf { color: #666666 } /* Literal.Number.Float */
|
||||||
|
.mh { color: #666666 } /* Literal.Number.Hex */
|
||||||
|
.mi { color: #666666 } /* Literal.Number.Integer */
|
||||||
|
.mo { color: #666666 } /* Literal.Number.Oct */
|
||||||
|
.sa { color: #BA2121 } /* Literal.String.Affix */
|
||||||
|
.sb { color: #BA2121 } /* Literal.String.Backtick */
|
||||||
|
.sc { color: #BA2121 } /* Literal.String.Char */
|
||||||
|
.dl { color: #BA2121 } /* Literal.String.Delimiter */
|
||||||
|
.sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
|
||||||
|
.s2 { color: #BA2121 } /* Literal.String.Double */
|
||||||
|
.se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */
|
||||||
|
.sh { color: #BA2121 } /* Literal.String.Heredoc */
|
||||||
|
.si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */
|
||||||
|
.sx { color: #008000 } /* Literal.String.Other */
|
||||||
|
.sr { color: #A45A77 } /* Literal.String.Regex */
|
||||||
|
.s1 { color: #BA2121 } /* Literal.String.Single */
|
||||||
|
.ss { color: #19177C } /* Literal.String.Symbol */
|
||||||
|
.bp { color: #008000 } /* Name.Builtin.Pseudo */
|
||||||
|
.fm { color: #0000FF } /* Name.Function.Magic */
|
||||||
|
.vc { color: #19177C } /* Name.Variable.Class */
|
||||||
|
.vg { color: #19177C } /* Name.Variable.Global */
|
||||||
|
.vi { color: #19177C } /* Name.Variable.Instance */
|
||||||
|
.vm { color: #19177C } /* Name.Variable.Magic */
|
||||||
|
.il { color: #666666 } /* Literal.Number.Integer.Long */
|
||||||
BIN
blag/static/favicon.ico
Normal file
BIN
blag/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
166
blag/static/style.css
Normal file
166
blag/static/style.css
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
@import "code-light.css" (prefers-color-scheme: light);
|
||||||
|
@import "code-dark.css" (prefers-color-scheme: dark);
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
--background: #FFFFFF;
|
||||||
|
--background-dim: #f5f7f9;
|
||||||
|
|
||||||
|
--foreground: #2B303A;
|
||||||
|
--foreground-dim: #576379;
|
||||||
|
--foreground-heavy: #191C22;
|
||||||
|
|
||||||
|
--primary-color: #375287;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--background: #2B363B;
|
||||||
|
--background-dim: #2F3C42;
|
||||||
|
|
||||||
|
--foreground: #f0f2f3;
|
||||||
|
--foreground-dim: #d5d5d5;
|
||||||
|
--foreground-heavy: #f2f4f5;
|
||||||
|
|
||||||
|
--primary-color: #A1C5FF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 18px;
|
||||||
|
font-family: serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 50rem;
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 0rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside {
|
||||||
|
font-size: smaller;
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--foreground-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
strong {
|
||||||
|
color: var(--foreground-heavy);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 a:hover, h2 a:hover, h3 a:hover, h4 a:hover, h5 a:hover, h6 a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav li {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav li + li:before {
|
||||||
|
content: " · ";
|
||||||
|
margin: 0 0.5ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
article header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
article header time {
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--foreground-dim);
|
||||||
|
font-style: italic;
|
||||||
|
flex: 0 0 12ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
article header h2,
|
||||||
|
article header p {
|
||||||
|
font-size: 1rem;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
code,
|
||||||
|
pre {
|
||||||
|
background: var(--background-dim);
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
padding: 1rem;
|
||||||
|
border-left: 2px solid var(--primary-color);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 0.1rem 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* reset the padding for code inside pre */
|
||||||
|
pre code {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
background: var(--background-dim);
|
||||||
|
border-radius: 0 0.3rem 0.3rem 0;
|
||||||
|
font-style: italic;
|
||||||
|
border-left: 2px solid var(--primary-color);
|
||||||
|
margin: 0;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* reset the margin for p inside blockquotes */
|
||||||
|
blockquote p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body > header {
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body footer {
|
||||||
|
margin: 3rem 0;
|
||||||
|
color: var(--foreground-dim);
|
||||||
|
font-size: smaller;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
header nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
margin: 0 auto;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
header h2 {
|
||||||
|
display: inline;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
@@ -1,20 +1,23 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}{{ site.title }}{% endblock %}
|
{% block title %}Archive{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% for entry in archive %}
|
{% for entry in archive %}
|
||||||
|
|
||||||
{% if entry.title %}
|
<article>
|
||||||
<h1><a href="{{entry.dst}}">{{entry.title}}</a></h1>
|
<header>
|
||||||
|
<time datetime="{{ entry.date }}">{{ entry.date.date() }}</time>
|
||||||
|
<div>
|
||||||
|
<h2><a href="{{ entry.dst }}">{{ entry.title }}</a></h2>
|
||||||
{% if entry.description %}
|
{% if entry.description %}
|
||||||
<p>— {{ entry.description }}</p>
|
<p>— {{ entry.description }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
</header>
|
||||||
|
</article>
|
||||||
<p>Written on {{ entry.date.date() }}.</p>
|
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
</p>
|
</p>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|
||||||
{{ content }}
|
{{ content }}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -4,12 +4,15 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="color-scheme" content="light dark">
|
||||||
<meta name="author" content="{{ site.author }}">
|
<meta name="author" content="{{ site.author }}">
|
||||||
{%- if description %}
|
{%- if description %}
|
||||||
<meta name="description" content="{{ description }}">
|
<meta name="description" content="{{ description }}">
|
||||||
{%- else %}
|
{%- else %}
|
||||||
<meta name="description" content="{{ site.description }}">
|
<meta name="description" content="{{ site.description }}">
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
<link rel="alternate" href="/atom.xml" type="application/atom+xml">
|
||||||
|
<link rel="stylesheet" href="/style.css" type="text/css">
|
||||||
<title>{% block title %}{% endblock %} | {{ site.description }}</title>
|
<title>{% block title %}{% endblock %} | {{ site.description }}</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -19,9 +22,10 @@
|
|||||||
<nav>
|
<nav>
|
||||||
<h2>{{ site.description }}</h2>
|
<h2>{{ site.description }}</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/">Blog</a></li>
|
<li><h2><a href="/">Blog</a></h2></li>
|
||||||
<li><a href="/tags/">Tags</a></li>
|
<li><h2><a href="/archive.html">Archive</a></h2></li>
|
||||||
<li><a href="/atom.xml">Atom Feed</a></li>
|
<li><h2><a href="/tags/">Tags</a></h2></li>
|
||||||
|
<li><h2><a href="/about.html">About Me</a></h2></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -31,7 +35,16 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>This website was built with <a href="https://github.com/venthur/blag">blag</a>.
|
||||||
|
<br>
|
||||||
|
Subscribe to the <a href="/atom.xml">atom feed</a>.
|
||||||
|
<br>
|
||||||
|
Contact me via
|
||||||
|
<a rel="me" href="https://mastodon.social/[FIXME]">[FIXME] Mastodon</a> or
|
||||||
|
<a href="https://github.com/[FIXME]">[FIXME] Github</a>.
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
25
blag/templates/index.html
Normal file
25
blag/templates/index.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ site.title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% for entry in archive[:15] %}
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<header>
|
||||||
|
<time datetime="{{ entry.date }}">{{ entry.date.date() }}</time>
|
||||||
|
<div>
|
||||||
|
<h2><a href="{{ entry.dst }}">{{ entry.title }}</a></h2>
|
||||||
|
{% if entry.description %}
|
||||||
|
<p>— {{ entry.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<p><a href="/archive.html">all articles...</a></p>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -3,5 +3,7 @@
|
|||||||
{% block title %}{{ title }}{% endblock %}
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{{ content }}
|
{{ content }}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Tag {{ tag }}{% endblock %}
|
{% block title %}#{{ tag }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
<h2>Articles tagged "{{ tag }}"</h2>
|
||||||
|
|
||||||
{% for entry in archive %}
|
{% for entry in archive %}
|
||||||
|
|
||||||
{% if entry.title %}
|
<article>
|
||||||
<h1><a href="/{{entry.dst}}">{{entry.title}}</a></h1>
|
<header>
|
||||||
|
<time datetime="{{ entry.date }}">{{ entry.date.date() }}</time>
|
||||||
|
<div>
|
||||||
|
<h2><a href="../{{ entry.dst }}">{{ entry.title }}</a></h2>
|
||||||
{% if entry.description %}
|
{% if entry.description %}
|
||||||
<p>— {{ entry.description }}</p>
|
<p>— {{ entry.description }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
</header>
|
||||||
|
</article>
|
||||||
<p>Written on {{ entry.date.date() }}.</p>
|
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__VERSION__ = '1.5.0'
|
__VERSION__ = "1.5.0"
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ Install blag from PyPI_
|
|||||||
|
|
||||||
.. _pypi: https://pypi.org/project/blag/
|
.. _pypi: https://pypi.org/project/blag/
|
||||||
|
|
||||||
Run blag's quickstart command to create the configuration and templates needed
|
Run blag's quickstart command to create the configuration, templates and some
|
||||||
|
initial content.
|
||||||
|
|
||||||
.. code-block:: sh
|
.. code-block:: sh
|
||||||
|
|
||||||
@@ -23,7 +24,6 @@ Create some content
|
|||||||
|
|
||||||
.. code-block:: sh
|
.. code-block:: sh
|
||||||
|
|
||||||
$ mkdir content
|
|
||||||
$ edit content/hello-world.md
|
$ edit content/hello-world.md
|
||||||
|
|
||||||
Generate the website
|
Generate the website
|
||||||
@@ -121,7 +121,7 @@ Static Files
|
|||||||
|
|
||||||
Static files can be put into the ``content`` directory and will be copied over
|
Static files can be put into the ``content`` directory and will be copied over
|
||||||
to the ``build`` directory as well. If you want better separation between
|
to the ``build`` directory as well. If you want better separation between
|
||||||
content and static files, you can create a ``static`` directory and put the
|
content and static files, you can use the ``static`` directory and put the
|
||||||
files there. All files and directories found in the ``static`` directory will
|
files there. All files and directories found in the ``static`` directory will
|
||||||
be copied over to ``build``.
|
be copied over to ``build``.
|
||||||
|
|
||||||
@@ -193,7 +193,8 @@ Template Used For Variables
|
|||||||
============ ====================================== ===================
|
============ ====================================== ===================
|
||||||
page.html pages (i.e. non-articles) site, content, meta
|
page.html pages (i.e. non-articles) site, content, meta
|
||||||
article.html articles (i.e. blog posts) site, content, meta
|
article.html articles (i.e. blog posts) site, content, meta
|
||||||
archive.html archive- and landing page of the blog site, archive
|
index.html landing page of the blog site, archive
|
||||||
|
archive.html archive page of the blog site, archive
|
||||||
tags.html list of tags site, tags
|
tags.html list of tags site, tags
|
||||||
tag.html archive of Articles with a certain tag site, archive, tag
|
tag.html archive of Articles with a certain tag site, archive, tag
|
||||||
============ ====================================== ===================
|
============ ====================================== ===================
|
||||||
|
|||||||
@@ -47,11 +47,15 @@ version = {attr = "blag.__VERSION__" }
|
|||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
packages = [
|
packages = [
|
||||||
"blag",
|
"blag",
|
||||||
"blag.templates",
|
"tests",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
blag = ["templates/*"]
|
blag = [
|
||||||
|
"templates/*",
|
||||||
|
"static/*",
|
||||||
|
"content/*",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
addopts = """
|
addopts = """
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
build==0.9.0
|
build==0.10.0
|
||||||
mkdocs==1.4.3
|
mkdocs==1.4.3
|
||||||
mkdocs-material==9.1.15
|
mkdocs-material==9.1.15
|
||||||
mkdocstrings[python]==0.20.0
|
mkdocstrings[python]==0.20.0
|
||||||
twine==4.0.2
|
twine==4.0.2
|
||||||
wheel==0.40.0
|
wheel==0.40.0
|
||||||
pytest==7.3.0
|
pytest==7.3.2
|
||||||
pytest-cov==4.0.0
|
pytest-cov==4.0.0
|
||||||
flake8==6.0.0
|
flake8==6.0.0
|
||||||
mypy==1.2.0
|
mypy==1.2.0
|
||||||
types-markdown==3.4.2.1
|
types-markdown==3.4.2.9
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
markdown==3.4.1
|
markdown==3.4.3
|
||||||
feedgenerator==2.0.0
|
feedgenerator==2.0.0
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
pygments==2.13.0
|
pygments==2.15.1
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
# remove when we don't support py38 anymore
|
# remove when we don't support py38 anymore
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from argparse import Namespace
|
|
||||||
from typing import Iterator, Callable
|
|
||||||
from tempfile import TemporaryDirectory
|
|
||||||
import os
|
import os
|
||||||
|
from argparse import Namespace
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
from typing import Callable, Iterator
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from jinja2 import Environment, Template
|
from jinja2 import Environment, Template
|
||||||
@@ -14,38 +15,43 @@ from blag import blag, quickstart
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def environment(cleandir: str) -> Iterator[Environment]:
|
def environment(cleandir: str) -> Iterator[Environment]:
|
||||||
site = {
|
site = {
|
||||||
'base_url': 'site base_url',
|
"base_url": "site base_url",
|
||||||
'title': 'site title',
|
"title": "site title",
|
||||||
'description': 'site description',
|
"description": "site description",
|
||||||
'author': 'site author',
|
"author": "site author",
|
||||||
}
|
}
|
||||||
env = blag.environment_factory('templates', globals_=dict(site=site))
|
env = blag.environment_factory("templates", globals_=dict(site=site))
|
||||||
yield env
|
yield env
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def page_template(environment: Environment) -> Iterator[Template]:
|
def page_template(environment: Environment) -> Iterator[Template]:
|
||||||
yield environment.get_template('page.html')
|
yield environment.get_template("page.html")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def article_template(environment: Environment) -> Iterator[Template]:
|
def article_template(environment: Environment) -> Iterator[Template]:
|
||||||
yield environment.get_template('article.html')
|
yield environment.get_template("article.html")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def index_template(environment: Environment) -> Iterator[Template]:
|
||||||
|
yield environment.get_template("index.html")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def archive_template(environment: Environment) -> Iterator[Template]:
|
def archive_template(environment: Environment) -> Iterator[Template]:
|
||||||
yield environment.get_template('archive.html')
|
yield environment.get_template("archive.html")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def tags_template(environment: Environment) -> Iterator[Template]:
|
def tags_template(environment: Environment) -> Iterator[Template]:
|
||||||
yield environment.get_template('tags.html')
|
yield environment.get_template("tags.html")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def tag_template(environment: Environment) -> Iterator[Template]:
|
def tag_template(environment: Environment) -> Iterator[Template]:
|
||||||
yield environment.get_template('tag.html')
|
yield environment.get_template("tag.html")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -60,14 +66,13 @@ author = a. u. thor
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
with TemporaryDirectory() as dir:
|
with TemporaryDirectory() as dir:
|
||||||
for d in 'content', 'build', 'static':
|
os.mkdir(f"{dir}/build")
|
||||||
os.mkdir(f'{dir}/{d}')
|
with open(f"{dir}/config.ini", "w") as fh:
|
||||||
with open(f'{dir}/config.ini', 'w') as fh:
|
|
||||||
fh.write(config)
|
fh.write(config)
|
||||||
# change directory
|
# change directory
|
||||||
old_cwd = os.getcwd()
|
old_cwd = os.getcwd()
|
||||||
os.chdir(dir)
|
os.chdir(dir)
|
||||||
quickstart.copy_templates()
|
quickstart.copy_default_theme()
|
||||||
yield dir
|
yield dir
|
||||||
# and change back afterwards
|
# and change back afterwards
|
||||||
os.chdir(old_cwd)
|
os.chdir(old_cwd)
|
||||||
@@ -75,11 +80,10 @@ author = a. u. thor
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def args(cleandir: Callable[[], Iterator[str]]) -> Iterator[Namespace]:
|
def args(cleandir: Callable[[], Iterator[str]]) -> Iterator[Namespace]:
|
||||||
|
|
||||||
args = Namespace(
|
args = Namespace(
|
||||||
input_dir='content',
|
input_dir="content",
|
||||||
output_dir='build',
|
output_dir="build",
|
||||||
static_dir='static',
|
static_dir="static",
|
||||||
template_dir='templates',
|
template_dir="templates",
|
||||||
)
|
)
|
||||||
yield args
|
yield args
|
||||||
|
|||||||
@@ -1,73 +1,73 @@
|
|||||||
# remove when we don't support py38 anymore
|
# remove when we don't support py38 anymore
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from tempfile import TemporaryDirectory
|
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
|
||||||
from typing import Any
|
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
|
from datetime import datetime
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pytest import CaptureFixture, LogCaptureFixture
|
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
|
from pytest import CaptureFixture, LogCaptureFixture
|
||||||
|
|
||||||
from blag import __VERSION__
|
from blag import __VERSION__, blag
|
||||||
from blag import blag
|
|
||||||
|
|
||||||
|
|
||||||
def test_generate_feed(cleandir: str) -> None:
|
def test_generate_feed(cleandir: str) -> None:
|
||||||
articles: list[tuple[str, dict[str, Any]]] = []
|
articles: list[tuple[str, dict[str, Any]]] = []
|
||||||
blag.generate_feed(articles, 'build', ' ', ' ', ' ', ' ')
|
blag.generate_feed(articles, "build", " ", " ", " ", " ")
|
||||||
assert os.path.exists('build/atom.xml')
|
assert os.path.exists("build/atom.xml")
|
||||||
|
|
||||||
|
|
||||||
def test_feed(cleandir: str) -> None:
|
def test_feed(cleandir: str) -> None:
|
||||||
articles: list[tuple[str, dict[str, Any]]] = [
|
articles: list[tuple[str, dict[str, Any]]] = [
|
||||||
(
|
(
|
||||||
'dest1.html',
|
"dest1.html",
|
||||||
{
|
{
|
||||||
'title': 'title1',
|
"title": "title1",
|
||||||
'date': datetime(2019, 6, 6),
|
"date": datetime(2019, 6, 6),
|
||||||
'content': 'content1',
|
"content": "content1",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'dest2.html',
|
"dest2.html",
|
||||||
{
|
{
|
||||||
'title': 'title2',
|
"title": "title2",
|
||||||
'date': datetime(1980, 5, 9),
|
"date": datetime(1980, 5, 9),
|
||||||
'content': 'content2',
|
"content": "content2",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
blag.generate_feed(
|
blag.generate_feed(
|
||||||
articles,
|
articles,
|
||||||
'build',
|
"build",
|
||||||
'https://example.com/',
|
"https://example.com/",
|
||||||
'blog title',
|
"blog title",
|
||||||
'blog description',
|
"blog description",
|
||||||
'blog author',
|
"blog author",
|
||||||
)
|
)
|
||||||
with open('build/atom.xml') as fh:
|
with open("build/atom.xml") as fh:
|
||||||
feed = fh.read()
|
feed = fh.read()
|
||||||
|
|
||||||
assert '<title>blog title</title>' in feed
|
assert "<title>blog title</title>" in feed
|
||||||
# enable when https://github.com/getpelican/feedgenerator/issues/22
|
# enable when https://github.com/getpelican/feedgenerator/issues/22
|
||||||
# is fixed
|
# is fixed
|
||||||
# assert '<subtitle>blog description</subtitle>' in feed
|
# assert '<subtitle>blog description</subtitle>' in feed
|
||||||
assert '<author><name>blog author</name></author>' in feed
|
assert "<author><name>blog author</name></author>" in feed
|
||||||
|
|
||||||
# article 1
|
# article 1
|
||||||
assert '<title>title1</title>' in feed
|
assert "<title>title1</title>" in feed
|
||||||
assert '<summary type="html">title1' in feed
|
assert '<summary type="html">title1' in feed
|
||||||
assert '<published>2019-06-06' in feed
|
assert "<published>2019-06-06" in feed
|
||||||
assert '<content type="html">content1' in feed
|
assert '<content type="html">content1' in feed
|
||||||
assert '<link href="https://example.com/dest1.html"' in feed
|
assert '<link href="https://example.com/dest1.html"' in feed
|
||||||
|
|
||||||
# article 2
|
# article 2
|
||||||
assert '<title>title2</title>' in feed
|
assert "<title>title2</title>" in feed
|
||||||
assert '<summary type="html">title2' in feed
|
assert '<summary type="html">title2' in feed
|
||||||
assert '<published>1980-05-09' in feed
|
assert "<published>1980-05-09" in feed
|
||||||
assert '<content type="html">content2' in feed
|
assert '<content type="html">content2' in feed
|
||||||
assert '<link href="https://example.com/dest2.html"' in feed
|
assert '<link href="https://example.com/dest2.html"' in feed
|
||||||
|
|
||||||
@@ -77,57 +77,57 @@ def test_generate_feed_with_description(cleandir: str) -> None:
|
|||||||
# the feed, otherwise we simply use the title of the article
|
# the feed, otherwise we simply use the title of the article
|
||||||
articles: list[tuple[str, dict[str, Any]]] = [
|
articles: list[tuple[str, dict[str, Any]]] = [
|
||||||
(
|
(
|
||||||
'dest.html',
|
"dest.html",
|
||||||
{
|
{
|
||||||
'title': 'title',
|
"title": "title",
|
||||||
'description': 'description',
|
"description": "description",
|
||||||
'date': datetime(2019, 6, 6),
|
"date": datetime(2019, 6, 6),
|
||||||
'content': 'content',
|
"content": "content",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
blag.generate_feed(articles, 'build', ' ', ' ', ' ', ' ')
|
blag.generate_feed(articles, "build", " ", " ", " ", " ")
|
||||||
|
|
||||||
with open('build/atom.xml') as fh:
|
with open("build/atom.xml") as fh:
|
||||||
feed = fh.read()
|
feed = fh.read()
|
||||||
|
|
||||||
assert '<title>title</title>' in feed
|
assert "<title>title</title>" in feed
|
||||||
assert '<summary type="html">description' in feed
|
assert '<summary type="html">description' in feed
|
||||||
assert '<published>2019-06-06' in feed
|
assert "<published>2019-06-06" in feed
|
||||||
assert '<content type="html">content' in feed
|
assert '<content type="html">content' in feed
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_build() -> None:
|
def test_parse_args_build() -> None:
|
||||||
# test default args
|
# test default args
|
||||||
args = blag.parse_args(['build'])
|
args = blag.parse_args(["build"])
|
||||||
assert args.input_dir == 'content'
|
assert args.input_dir == "content"
|
||||||
assert args.output_dir == 'build'
|
assert args.output_dir == "build"
|
||||||
assert args.template_dir == 'templates'
|
assert args.template_dir == "templates"
|
||||||
assert args.static_dir == 'static'
|
assert args.static_dir == "static"
|
||||||
|
|
||||||
# input dir
|
# input dir
|
||||||
args = blag.parse_args(['build', '-i', 'foo'])
|
args = blag.parse_args(["build", "-i", "foo"])
|
||||||
assert args.input_dir == 'foo'
|
assert args.input_dir == "foo"
|
||||||
args = blag.parse_args(['build', '--input-dir', 'foo'])
|
args = blag.parse_args(["build", "--input-dir", "foo"])
|
||||||
assert args.input_dir == 'foo'
|
assert args.input_dir == "foo"
|
||||||
|
|
||||||
# output dir
|
# output dir
|
||||||
args = blag.parse_args(['build', '-o', 'foo'])
|
args = blag.parse_args(["build", "-o", "foo"])
|
||||||
assert args.output_dir == 'foo'
|
assert args.output_dir == "foo"
|
||||||
args = blag.parse_args(['build', '--output-dir', 'foo'])
|
args = blag.parse_args(["build", "--output-dir", "foo"])
|
||||||
assert args.output_dir == 'foo'
|
assert args.output_dir == "foo"
|
||||||
|
|
||||||
# template dir
|
# template dir
|
||||||
args = blag.parse_args(['build', '-t', 'foo'])
|
args = blag.parse_args(["build", "-t", "foo"])
|
||||||
assert args.template_dir == 'foo'
|
assert args.template_dir == "foo"
|
||||||
args = blag.parse_args(['build', '--template-dir', 'foo'])
|
args = blag.parse_args(["build", "--template-dir", "foo"])
|
||||||
assert args.template_dir == 'foo'
|
assert args.template_dir == "foo"
|
||||||
|
|
||||||
# static dir
|
# static dir
|
||||||
args = blag.parse_args(['build', '-s', 'foo'])
|
args = blag.parse_args(["build", "-s", "foo"])
|
||||||
assert args.static_dir == 'foo'
|
assert args.static_dir == "foo"
|
||||||
args = blag.parse_args(['build', '--static-dir', 'foo'])
|
args = blag.parse_args(["build", "--static-dir", "foo"])
|
||||||
assert args.static_dir == 'foo'
|
assert args.static_dir == "foo"
|
||||||
|
|
||||||
|
|
||||||
def test_get_config() -> None:
|
def test_get_config() -> None:
|
||||||
@@ -140,24 +140,24 @@ author = a. u. thor
|
|||||||
"""
|
"""
|
||||||
# happy path
|
# happy path
|
||||||
with TemporaryDirectory() as dir:
|
with TemporaryDirectory() as dir:
|
||||||
configfile = f'{dir}/config.ini'
|
configfile = f"{dir}/config.ini"
|
||||||
with open(configfile, 'w') as fh:
|
with open(configfile, "w") as fh:
|
||||||
fh.write(config)
|
fh.write(config)
|
||||||
|
|
||||||
config_parsed = blag.get_config(configfile)
|
config_parsed = blag.get_config(configfile)
|
||||||
assert config_parsed['base_url'] == 'https://example.com/'
|
assert config_parsed["base_url"] == "https://example.com/"
|
||||||
assert config_parsed['title'] == 'title'
|
assert config_parsed["title"] == "title"
|
||||||
assert config_parsed['description'] == 'description'
|
assert config_parsed["description"] == "description"
|
||||||
assert config_parsed['author'] == 'a. u. thor'
|
assert config_parsed["author"] == "a. u. thor"
|
||||||
|
|
||||||
# a missing required config causes a sys.exit
|
# a missing required config causes a sys.exit
|
||||||
for x in 'base_url', 'title', 'description', 'author':
|
for x in "base_url", "title", "description", "author":
|
||||||
config2 = '\n'.join(
|
config2 = "\n".join(
|
||||||
[line for line in config.splitlines() if not line.startswith(x)]
|
[line for line in config.splitlines() if not line.startswith(x)]
|
||||||
)
|
)
|
||||||
with TemporaryDirectory() as dir:
|
with TemporaryDirectory() as dir:
|
||||||
configfile = f'{dir}/config.ini'
|
configfile = f"{dir}/config.ini"
|
||||||
with open(configfile, 'w') as fh:
|
with open(configfile, "w") as fh:
|
||||||
fh.write(config2)
|
fh.write(config2)
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
config_parsed = blag.get_config(configfile)
|
config_parsed = blag.get_config(configfile)
|
||||||
@@ -171,19 +171,19 @@ description = description
|
|||||||
author = a. u. thor
|
author = a. u. thor
|
||||||
"""
|
"""
|
||||||
with TemporaryDirectory() as dir:
|
with TemporaryDirectory() as dir:
|
||||||
configfile = f'{dir}/config.ini'
|
configfile = f"{dir}/config.ini"
|
||||||
with open(configfile, 'w') as fh:
|
with open(configfile, "w") as fh:
|
||||||
fh.write(config)
|
fh.write(config)
|
||||||
|
|
||||||
config_parsed = blag.get_config(configfile)
|
config_parsed = blag.get_config(configfile)
|
||||||
assert config_parsed['base_url'] == 'https://example.com/'
|
assert config_parsed["base_url"] == "https://example.com/"
|
||||||
|
|
||||||
|
|
||||||
def test_environment_factory(cleandir: str) -> None:
|
def test_environment_factory(cleandir: str) -> None:
|
||||||
globals_: dict[str, object] = {'foo': 'bar', 'test': 'me'}
|
globals_: dict[str, object] = {"foo": "bar", "test": "me"}
|
||||||
env = blag.environment_factory("templates", globals_=globals_)
|
env = blag.environment_factory("templates", globals_=globals_)
|
||||||
assert env.globals['foo'] == 'bar'
|
assert env.globals["foo"] == "bar"
|
||||||
assert env.globals['test'] == 'me'
|
assert env.globals["test"] == "me"
|
||||||
|
|
||||||
|
|
||||||
def test_process_markdown(
|
def test_process_markdown(
|
||||||
@@ -216,12 +216,12 @@ foo bar
|
|||||||
|
|
||||||
convertibles = []
|
convertibles = []
|
||||||
for i, txt in enumerate((page1, article1, article2)):
|
for i, txt in enumerate((page1, article1, article2)):
|
||||||
with open(f'content/{str(i)}', 'w') as fh:
|
with open(f"content/{str(i)}", "w") as fh:
|
||||||
fh.write(txt)
|
fh.write(txt)
|
||||||
convertibles.append((str(i), str(i)))
|
convertibles.append((str(i), str(i)))
|
||||||
|
|
||||||
articles, pages = blag.process_markdown(
|
articles, pages = blag.process_markdown(
|
||||||
convertibles, 'content', 'build', page_template, article_template
|
convertibles, "content", "build", page_template, article_template
|
||||||
)
|
)
|
||||||
|
|
||||||
assert isinstance(articles, list)
|
assert isinstance(articles, list)
|
||||||
@@ -229,14 +229,14 @@ foo bar
|
|||||||
for dst, context in articles:
|
for dst, context in articles:
|
||||||
assert isinstance(dst, str)
|
assert isinstance(dst, str)
|
||||||
assert isinstance(context, dict)
|
assert isinstance(context, dict)
|
||||||
assert 'content' in context
|
assert "content" in context
|
||||||
|
|
||||||
assert isinstance(pages, list)
|
assert isinstance(pages, list)
|
||||||
assert len(pages) == 1
|
assert len(pages) == 1
|
||||||
for dst, context in pages:
|
for dst, context in pages:
|
||||||
assert isinstance(dst, str)
|
assert isinstance(dst, str)
|
||||||
assert isinstance(context, dict)
|
assert isinstance(context, dict)
|
||||||
assert 'content' in context
|
assert "content" in context
|
||||||
|
|
||||||
|
|
||||||
def test_build(args: Namespace) -> None:
|
def test_build(args: Namespace) -> None:
|
||||||
@@ -268,60 +268,63 @@ foo bar
|
|||||||
# write some convertibles
|
# write some convertibles
|
||||||
convertibles = []
|
convertibles = []
|
||||||
for i, txt in enumerate((page1, article1, article2)):
|
for i, txt in enumerate((page1, article1, article2)):
|
||||||
with open(f'{args.input_dir}/{str(i)}.md', 'w') as fh:
|
with open(f"{args.input_dir}/{str(i)}.md", "w") as fh:
|
||||||
fh.write(txt)
|
fh.write(txt)
|
||||||
convertibles.append((str(i), str(i)))
|
convertibles.append((str(i), str(i)))
|
||||||
|
|
||||||
# some static files
|
# some static files
|
||||||
with open(f'{args.static_dir}/test', 'w') as fh:
|
with open(f"{args.static_dir}/test", "w") as fh:
|
||||||
fh.write('hello')
|
fh.write("hello")
|
||||||
|
|
||||||
os.mkdir(f'{args.input_dir}/testdir')
|
os.mkdir(f"{args.input_dir}/testdir")
|
||||||
with open(f'{args.input_dir}/testdir/test', 'w') as fh:
|
with open(f"{args.input_dir}/testdir/test", "w") as fh:
|
||||||
fh.write('hello')
|
fh.write("hello")
|
||||||
|
|
||||||
blag.build(args)
|
blag.build(args)
|
||||||
|
|
||||||
# test existence of the three converted files
|
# test existence of the three converted files
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
assert os.path.exists(f'{args.output_dir}/{i}.html')
|
assert os.path.exists(f"{args.output_dir}/{i}.html")
|
||||||
# ... static file
|
# ... static file
|
||||||
assert os.path.exists(f'{args.output_dir}/test')
|
assert os.path.exists(f"{args.output_dir}/test")
|
||||||
# ... directory
|
# ... directory
|
||||||
assert os.path.exists(f'{args.output_dir}/testdir/test')
|
assert os.path.exists(f"{args.output_dir}/testdir/test")
|
||||||
# ... feed
|
# ... feed
|
||||||
assert os.path.exists(f'{args.output_dir}/atom.xml')
|
assert os.path.exists(f"{args.output_dir}/atom.xml")
|
||||||
|
# ... index
|
||||||
|
assert os.path.exists(f"{args.output_dir}/index.html")
|
||||||
# ... archive
|
# ... archive
|
||||||
assert os.path.exists(f'{args.output_dir}/index.html')
|
assert os.path.exists(f"{args.output_dir}/archive.html")
|
||||||
# ... tags
|
# ... tags
|
||||||
assert os.path.exists(f'{args.output_dir}/tags/index.html')
|
assert os.path.exists(f"{args.output_dir}/tags/index.html")
|
||||||
assert os.path.exists(f'{args.output_dir}/tags/foo.html')
|
assert os.path.exists(f"{args.output_dir}/tags/foo.html")
|
||||||
assert os.path.exists(f'{args.output_dir}/tags/bar.html')
|
assert os.path.exists(f"{args.output_dir}/tags/bar.html")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'template',
|
"template",
|
||||||
[
|
[
|
||||||
'page.html',
|
"page.html",
|
||||||
'article.html',
|
"article.html",
|
||||||
'archive.html',
|
"index.html",
|
||||||
'tags.html',
|
"archive.html",
|
||||||
'tag.html',
|
"tags.html",
|
||||||
]
|
"tag.html",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
def test_missing_template_raises(template: str, args: Namespace) -> None:
|
def test_missing_template_raises(template: str, args: Namespace) -> None:
|
||||||
os.remove(f'templates/{template}')
|
os.remove(f"templates/{template}")
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
blag.build(args)
|
blag.build(args)
|
||||||
|
|
||||||
|
|
||||||
def test_main(cleandir: str) -> None:
|
def test_main(cleandir: str) -> None:
|
||||||
blag.main(['build'])
|
blag.main(["build"])
|
||||||
|
|
||||||
|
|
||||||
def test_cli_version(capsys: CaptureFixture[str]) -> None:
|
def test_cli_version(capsys: CaptureFixture[str]) -> None:
|
||||||
with pytest.raises(SystemExit) as ex:
|
with pytest.raises(SystemExit) as ex:
|
||||||
blag.main(['--version'])
|
blag.main(["--version"])
|
||||||
# normal system exit
|
# normal system exit
|
||||||
assert ex.value.code == 0
|
assert ex.value.code == 0
|
||||||
# proper version reported
|
# proper version reported
|
||||||
@@ -330,8 +333,8 @@ def test_cli_version(capsys: CaptureFixture[str]) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_cli_verbose(cleandir: str, caplog: LogCaptureFixture) -> None:
|
def test_cli_verbose(cleandir: str, caplog: LogCaptureFixture) -> None:
|
||||||
blag.main(['build'])
|
blag.main(["build"])
|
||||||
assert 'DEBUG' not in caplog.text
|
assert "DEBUG" not in caplog.text
|
||||||
|
|
||||||
blag.main(['--verbose', 'build'])
|
blag.main(["--verbose", "build"])
|
||||||
assert 'DEBUG' in caplog.text
|
assert "DEBUG" in caplog.text
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# remove when we don't support py38 anymore
|
# remove when we don't support py38 anymore
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import time
|
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -11,17 +12,17 @@ from blag import devserver
|
|||||||
|
|
||||||
def test_get_last_modified(cleandir: str) -> None:
|
def test_get_last_modified(cleandir: str) -> None:
|
||||||
# take initial time
|
# take initial time
|
||||||
t1 = devserver.get_last_modified(['content'])
|
t1 = devserver.get_last_modified(["content"])
|
||||||
|
|
||||||
# wait a bit, create a file and measure again
|
# wait a bit, create a file and measure again
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
with open('content/test', 'w') as fh:
|
with open("content/test", "w") as fh:
|
||||||
fh.write('boo')
|
fh.write("boo")
|
||||||
t2 = devserver.get_last_modified(['content'])
|
t2 = devserver.get_last_modified(["content"])
|
||||||
|
|
||||||
# wait a bit and take time again
|
# wait a bit and take time again
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
t3 = devserver.get_last_modified(['content'])
|
t3 = devserver.get_last_modified(["content"])
|
||||||
|
|
||||||
assert t2 > t1
|
assert t2 > t1
|
||||||
assert t2 == t3
|
assert t2 == t3
|
||||||
@@ -29,20 +30,20 @@ def test_get_last_modified(cleandir: str) -> None:
|
|||||||
|
|
||||||
def test_autoreload_builds_immediately(args: Namespace) -> None:
|
def test_autoreload_builds_immediately(args: Namespace) -> None:
|
||||||
# create a dummy file that can be build
|
# create a dummy file that can be build
|
||||||
with open('content/test.md', 'w') as fh:
|
with open("content/test.md", "w") as fh:
|
||||||
fh.write('boo')
|
fh.write("boo")
|
||||||
|
|
||||||
t = threading.Thread(
|
t = threading.Thread(
|
||||||
target=devserver.autoreload,
|
target=devserver.autoreload,
|
||||||
args=(args,),
|
args=(args,),
|
||||||
daemon=True,
|
daemon=True,
|
||||||
)
|
)
|
||||||
t0 = devserver.get_last_modified(['build'])
|
t0 = devserver.get_last_modified(["build"])
|
||||||
t.start()
|
t.start()
|
||||||
# try for 5 seconds...
|
# try for 5 seconds...
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
t1 = devserver.get_last_modified(['build'])
|
t1 = devserver.get_last_modified(["build"])
|
||||||
print(t1)
|
print(t1)
|
||||||
if t1 > t0:
|
if t1 > t0:
|
||||||
break
|
break
|
||||||
@@ -60,16 +61,16 @@ def test_autoreload(args: Namespace) -> None:
|
|||||||
)
|
)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
t0 = devserver.get_last_modified(['build'])
|
t0 = devserver.get_last_modified(["build"])
|
||||||
|
|
||||||
# create a dummy file that can be build
|
# create a dummy file that can be build
|
||||||
with open('content/test.md', 'w') as fh:
|
with open("content/test.md", "w") as fh:
|
||||||
fh.write('boo')
|
fh.write("boo")
|
||||||
|
|
||||||
# try for 5 seconds to see if we rebuild once...
|
# try for 5 seconds to see if we rebuild once...
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
t1 = devserver.get_last_modified(['build'])
|
t1 = devserver.get_last_modified(["build"])
|
||||||
if t1 > t0:
|
if t1 > t0:
|
||||||
break
|
break
|
||||||
assert t1 > t0
|
assert t1 > t0
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
# remove when we don't support py38 anymore
|
# remove when we don't support py38 anymore
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
|
||||||
import markdown
|
import markdown
|
||||||
|
import pytest
|
||||||
|
|
||||||
from blag.markdown import convert_markdown, markdown_factory
|
from blag.markdown import convert_markdown, markdown_factory
|
||||||
|
|
||||||
@@ -13,23 +14,23 @@ from blag.markdown import convert_markdown, markdown_factory
|
|||||||
"input_, expected",
|
"input_, expected",
|
||||||
[
|
[
|
||||||
# inline
|
# inline
|
||||||
('[test](test.md)', 'test.html'),
|
("[test](test.md)", "test.html"),
|
||||||
('[test](test.md "test")', 'test.html'),
|
('[test](test.md "test")', "test.html"),
|
||||||
('[test](a/test.md)', 'a/test.html'),
|
("[test](a/test.md)", "a/test.html"),
|
||||||
('[test](a/test.md "test")', 'a/test.html'),
|
('[test](a/test.md "test")', "a/test.html"),
|
||||||
('[test](/test.md)', '/test.html'),
|
("[test](/test.md)", "/test.html"),
|
||||||
('[test](/test.md "test")', '/test.html'),
|
('[test](/test.md "test")', "/test.html"),
|
||||||
('[test](/a/test.md)', '/a/test.html'),
|
("[test](/a/test.md)", "/a/test.html"),
|
||||||
('[test](/a/test.md "test")', '/a/test.html'),
|
('[test](/a/test.md "test")', "/a/test.html"),
|
||||||
# reference
|
# reference
|
||||||
('[test][]\n[test]: test.md ' '', 'test.html'),
|
("[test][]\n[test]: test.md " "", "test.html"),
|
||||||
('[test][]\n[test]: test.md "test"', 'test.html'),
|
('[test][]\n[test]: test.md "test"', "test.html"),
|
||||||
('[test][]\n[test]: a/test.md', 'a/test.html'),
|
("[test][]\n[test]: a/test.md", "a/test.html"),
|
||||||
('[test][]\n[test]: a/test.md "test"', 'a/test.html'),
|
('[test][]\n[test]: a/test.md "test"', "a/test.html"),
|
||||||
('[test][]\n[test]: /test.md', '/test.html'),
|
("[test][]\n[test]: /test.md", "/test.html"),
|
||||||
('[test][]\n[test]: /test.md "test"', '/test.html'),
|
('[test][]\n[test]: /test.md "test"', "/test.html"),
|
||||||
('[test][]\n[test]: /a/test.md', '/a/test.html'),
|
("[test][]\n[test]: /a/test.md", "/a/test.html"),
|
||||||
('[test][]\n[test]: /a/test.md "test"', '/a/test.html'),
|
('[test][]\n[test]: /a/test.md "test"', "/a/test.html"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_convert_markdown_links(input_: str, expected: str) -> None:
|
def test_convert_markdown_links(input_: str, expected: str) -> None:
|
||||||
@@ -42,11 +43,11 @@ def test_convert_markdown_links(input_: str, expected: str) -> None:
|
|||||||
"input_, expected",
|
"input_, expected",
|
||||||
[
|
[
|
||||||
# scheme
|
# scheme
|
||||||
('[test](https://)', 'https://'),
|
("[test](https://)", "https://"),
|
||||||
# netloc
|
# netloc
|
||||||
('[test](//test.md)', '//test.md'),
|
("[test](//test.md)", "//test.md"),
|
||||||
# no path
|
# no path
|
||||||
('[test]()', ''),
|
("[test]()", ""),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_dont_convert_normal_links(input_: str, expected: str) -> None:
|
def test_dont_convert_normal_links(input_: str, expected: str) -> None:
|
||||||
@@ -58,13 +59,13 @@ def test_dont_convert_normal_links(input_: str, expected: str) -> None:
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"input_, expected",
|
"input_, expected",
|
||||||
[
|
[
|
||||||
('foo: bar', {'foo': 'bar'}),
|
("foo: bar", {"foo": "bar"}),
|
||||||
('foo: those are several words', {'foo': 'those are several words'}),
|
("foo: those are several words", {"foo": "those are several words"}),
|
||||||
('tags: this, is, a, test\n', {'tags': ['this', 'is', 'a', 'test']}),
|
("tags: this, is, a, test\n", {"tags": ["this", "is", "a", "test"]}),
|
||||||
('tags: this, IS, a, test', {'tags': ['this', 'is', 'a', 'test']}),
|
("tags: this, IS, a, test", {"tags": ["this", "is", "a", "test"]}),
|
||||||
(
|
(
|
||||||
'date: 2020-01-01 12:10',
|
"date: 2020-01-01 12:10",
|
||||||
{'date': datetime(2020, 1, 1, 12, 10).astimezone()},
|
{"date": datetime(2020, 1, 1, 12, 10).astimezone()},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -88,9 +89,9 @@ this --- is -- a test ...
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
html, meta = convert_markdown(md, md1)
|
html, meta = convert_markdown(md, md1)
|
||||||
assert 'mdash' in html
|
assert "mdash" in html
|
||||||
assert 'ndash' in html
|
assert "ndash" in html
|
||||||
assert 'hellip' in html
|
assert "hellip" in html
|
||||||
|
|
||||||
|
|
||||||
def test_smarty_code() -> None:
|
def test_smarty_code() -> None:
|
||||||
@@ -102,6 +103,6 @@ this --- is -- a test ...
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
html, meta = convert_markdown(md, md1)
|
html, meta = convert_markdown(md, md1)
|
||||||
assert 'mdash' not in html
|
assert "mdash" not in html
|
||||||
assert 'ndash' not in html
|
assert "ndash" not in html
|
||||||
assert 'hellip' not in html
|
assert "hellip" not in html
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# remove when we don't support py38 anymore
|
# remove when we don't support py38 anymore
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from pytest import MonkeyPatch
|
from pytest import MonkeyPatch
|
||||||
@@ -8,33 +9,37 @@ from blag.quickstart import get_input, quickstart
|
|||||||
|
|
||||||
|
|
||||||
def test_get_input_default_answer(monkeypatch: MonkeyPatch) -> None:
|
def test_get_input_default_answer(monkeypatch: MonkeyPatch) -> None:
|
||||||
monkeypatch.setattr('builtins.input', lambda x: '')
|
monkeypatch.setattr("builtins.input", lambda x: "")
|
||||||
answer = get_input("foo", "bar")
|
answer = get_input("foo", "bar")
|
||||||
assert answer == 'bar'
|
assert answer == "bar"
|
||||||
|
|
||||||
|
|
||||||
def test_get_input(monkeypatch: MonkeyPatch) -> None:
|
def test_get_input(monkeypatch: MonkeyPatch) -> None:
|
||||||
monkeypatch.setattr('builtins.input', lambda x: 'baz')
|
monkeypatch.setattr("builtins.input", lambda x: "baz")
|
||||||
answer = get_input("foo", "bar")
|
answer = get_input("foo", "bar")
|
||||||
assert answer == 'baz'
|
assert answer == "baz"
|
||||||
|
|
||||||
|
|
||||||
def test_quickstart(cleandir: str, monkeypatch: MonkeyPatch) -> None:
|
def test_quickstart(cleandir: str, monkeypatch: MonkeyPatch) -> None:
|
||||||
monkeypatch.setattr('builtins.input', lambda x: 'foo')
|
monkeypatch.setattr("builtins.input", lambda x: "foo")
|
||||||
quickstart(None)
|
quickstart(None)
|
||||||
with open('config.ini', 'r') as fh:
|
with open("config.ini", "r") as fh:
|
||||||
data = fh.read()
|
data = fh.read()
|
||||||
assert 'base_url = foo' in data
|
assert "base_url = foo" in data
|
||||||
assert 'title = foo' in data
|
assert "title = foo" in data
|
||||||
assert 'description = foo' in data
|
assert "description = foo" in data
|
||||||
assert 'author = foo' in data
|
assert "author = foo" in data
|
||||||
|
|
||||||
for template in (
|
for template in (
|
||||||
"archive.html",
|
"archive.html",
|
||||||
"article.html",
|
"article.html",
|
||||||
"base.html",
|
"base.html",
|
||||||
|
"index.html",
|
||||||
"page.html",
|
"page.html",
|
||||||
"tag.html",
|
"tag.html",
|
||||||
"tags.html",
|
"tags.html",
|
||||||
):
|
):
|
||||||
assert os.path.exists(f'templates/{template}')
|
assert os.path.exists(f"templates/{template}")
|
||||||
|
|
||||||
|
for directory in "build", "content", "static":
|
||||||
|
assert os.path.exists(directory)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# remove when we don't support py38 anymore
|
# remove when we don't support py38 anymore
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
@@ -7,71 +8,91 @@ from jinja2 import Template
|
|||||||
|
|
||||||
def test_page(page_template: Template) -> None:
|
def test_page(page_template: Template) -> None:
|
||||||
ctx = {
|
ctx = {
|
||||||
'content': 'this is the content',
|
"content": "this is the content",
|
||||||
'title': 'this is the title',
|
"title": "this is the title",
|
||||||
}
|
}
|
||||||
result = page_template.render(ctx)
|
result = page_template.render(ctx)
|
||||||
assert 'this is the content' in result
|
assert "this is the content" in result
|
||||||
assert 'this is the title' in result
|
assert "this is the title" in result
|
||||||
|
|
||||||
|
|
||||||
def test_article(article_template: Template) -> None:
|
def test_article(article_template: Template) -> None:
|
||||||
ctx = {
|
ctx = {
|
||||||
'content': 'this is the content',
|
"content": "this is the content",
|
||||||
'title': 'this is the title',
|
"title": "this is the title",
|
||||||
'date': datetime.datetime(1980, 5, 9),
|
"date": datetime.datetime(1980, 5, 9),
|
||||||
}
|
}
|
||||||
result = article_template.render(ctx)
|
result = article_template.render(ctx)
|
||||||
assert 'this is the content' in result
|
assert "this is the content" in result
|
||||||
assert 'this is the title' in result
|
assert "this is the title" in result
|
||||||
assert '1980-05-09' in result
|
assert "1980-05-09" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_index(index_template: Template) -> None:
|
||||||
|
entry = {
|
||||||
|
"title": "this is a title",
|
||||||
|
"dst": "https://example.com/link",
|
||||||
|
"date": datetime.datetime(1980, 5, 9),
|
||||||
|
}
|
||||||
|
archive = [entry]
|
||||||
|
ctx = {
|
||||||
|
"archive": archive,
|
||||||
|
}
|
||||||
|
result = index_template.render(ctx)
|
||||||
|
assert "site title" in result
|
||||||
|
|
||||||
|
assert "this is a title" in result
|
||||||
|
assert "1980-05-09" in result
|
||||||
|
assert "https://example.com/link" in result
|
||||||
|
|
||||||
|
assert "/archive.html" in result
|
||||||
|
|
||||||
|
|
||||||
def test_archive(archive_template: Template) -> None:
|
def test_archive(archive_template: Template) -> None:
|
||||||
entry = {
|
entry = {
|
||||||
'title': 'this is a title',
|
"title": "this is a title",
|
||||||
'dst': 'https://example.com/link',
|
"dst": "https://example.com/link",
|
||||||
'date': datetime.datetime(1980, 5, 9),
|
"date": datetime.datetime(1980, 5, 9),
|
||||||
}
|
}
|
||||||
archive = [entry]
|
archive = [entry]
|
||||||
ctx = {
|
ctx = {
|
||||||
'archive': archive,
|
"archive": archive,
|
||||||
}
|
}
|
||||||
result = archive_template.render(ctx)
|
result = archive_template.render(ctx)
|
||||||
assert 'site title' in result
|
assert "Archive" in result
|
||||||
|
|
||||||
assert 'this is a title' in result
|
assert "this is a title" in result
|
||||||
assert '1980-05-09' in result
|
assert "1980-05-09" in result
|
||||||
assert 'https://example.com/link' in result
|
assert "https://example.com/link" in result
|
||||||
|
|
||||||
|
|
||||||
def test_tags(tags_template: Template) -> None:
|
def test_tags(tags_template: Template) -> None:
|
||||||
tags = [('foo', 42)]
|
tags = [("foo", 42)]
|
||||||
ctx = {
|
ctx = {
|
||||||
'tags': tags,
|
"tags": tags,
|
||||||
}
|
}
|
||||||
result = tags_template.render(ctx)
|
result = tags_template.render(ctx)
|
||||||
assert 'Tags' in result
|
assert "Tags" in result
|
||||||
|
|
||||||
assert 'foo.html' in result
|
assert "foo.html" in result
|
||||||
assert 'foo' in result
|
assert "foo" in result
|
||||||
assert '42' in result
|
assert "42" in result
|
||||||
|
|
||||||
|
|
||||||
def test_tag(tag_template: Template) -> None:
|
def test_tag(tag_template: Template) -> None:
|
||||||
entry = {
|
entry = {
|
||||||
'title': 'this is a title',
|
"title": "this is a title",
|
||||||
'dst': 'https://example.com/link',
|
"dst": "https://example.com/link",
|
||||||
'date': datetime.datetime(1980, 5, 9),
|
"date": datetime.datetime(1980, 5, 9),
|
||||||
}
|
}
|
||||||
archive = [entry]
|
archive = [entry]
|
||||||
ctx = {
|
ctx = {
|
||||||
'tag': 'foo',
|
"tag": "foo",
|
||||||
'archive': archive,
|
"archive": archive,
|
||||||
}
|
}
|
||||||
result = tag_template.render(ctx)
|
result = tag_template.render(ctx)
|
||||||
assert 'Tag foo' in result
|
assert "foo" in result
|
||||||
|
|
||||||
assert 'this is a title' in result
|
assert "this is a title" in result
|
||||||
assert '1980-05-09' in result
|
assert "1980-05-09" in result
|
||||||
assert 'https://example.com/link' in result
|
assert "https://example.com/link" in result
|
||||||
|
|||||||
Reference in New Issue
Block a user