{{ entry.title }}
+ {% if entry.description %} +— {{ entry.description }}
+ {% endif %} +diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6a7695c..4776211 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,3 +4,8 @@ updates: directory: "/" schedule: interval: "weekly" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/python-package.yaml b/.github/workflows/python-package.yaml index fdccf0d..a938dac 100644 --- a/.github/workflows/python-package.yaml +++ b/.github/workflows/python-package.yaml @@ -20,19 +20,63 @@ jobs: - "3.8" - "3.9" - "3.10" + - "3.11" + - "3.12" + exclude: + # 3.8 on windows fails due to some pip issue + - os: windows-latest + python-version: "3.8" steps: - - uses: actions/checkout@v2 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - name: Run tests - run: | + - run: | + make venv + - run: | make test - - name: Run linter - run: | + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: "3.x" + + - run: | + make venv + - run: | make lint + + mypy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: "3.x" + + - run: | + make venv + - run: | + make mypy + + + test-release: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: "3.x" + + - run: | + make venv + - run: | + make test-release diff --git a/.gitignore b/.gitignore index 4780a71..65fa4d1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,10 @@ build/ dist/ *.egg-info/ -docs/_build/ -docs/api/ +site/ htmlcov/ .coverage +.mypy_cache venv/ diff --git a/.readthedocs.yaml b/.readthedocs.yaml index f076753..150d739 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,7 +1,14 @@ version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +mkdocs: + configuration: mkdocs.yml + python: - version: 3.8 install: - requirements: requirements.txt - requirements: requirements-dev.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 3009093..f4a94ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,113 @@ # Changelog -## unreleased +## [2.2.1] -- 2023-11-11 +* fixed `suggests` to blag-doc + +## [2.2.0] -- 2023-11-05 + +* switched from flake8 to ruff +* added missing docstrings +* fixed dev requirements in pyproject, still pointing to sphinx +* added Python3.12 to test suite +* removed debian/watch + +## [2.1.0] -- 2023-08-27 + +* default theme: `img` have now `max-width: 100%` by default to avoid very + large images overflowing +* packaging: explicitly list `templates`, `static` and `content` as packages + instead of relying on package-data for setuptools. additionally, created a + MANIFEST.in to add the contents of these directories here as well. the + automatic finding of namespace packages and packaga-data, currently does not + work as advertised in setuptools' docs +* updated dependencies +* created debian/watch + +## [2.0.0] - 2023-06-16 + +### Breaking + +* blag does not use default fallback templates anymore and will return an error + if it is unable to find required templates, e.g. in `templates/`. + + Users upgrading from older versions can either run `blag quickstart` (don't + forget to backup your `config.ini` or copy the templates from blag's + resources (the resource path is shown in the error message). + + New users are not affected as `blag quickstart` will generate the needed + 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 + +* Switched from sphinx to mkdocs + +### Fixed + +* fixed pyproject.toml to include tests/conftest.py + + +## [1.5.0] - 2023-04-16 + +* moved to pyproject.toml +* added python 3.11 to test suite +* break out lint and mypy from test matrix and only run on linux- and latest + stable python to make it a bit more efficient +* added dependabot check for github actions +* updated dependencies: + * mypy 1.2.0 + * types-markdown 3.4.2.1 + * pytest-cov 4.0.0 + * sphinx 5.3.0 + * pytest 7.3.0 + * flake8 6.0.0 + * twine 4.0.2 + * wheel 0.40.0 + +## [1.4.1] - 2022-09-29 + +* applied multi-arch fix by debian-janitor +* updated dependencies: + * pytest 7.1.3 + * sphinx 5.2.1 + * types-markdown 3.4.2 + +## [1.4.0] - 2022-09-01 + +* added type hints and mypy --strict to test suite * improved default template * updated dependencies: * markdown 3.4.1 - * flake 5.0.2 + * pygments 2.13.0 + * flake 5.0.4 * twine 4.0.1 * sphinx 5.1.1 diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..dbb52ef --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +recursive-include blag/content * +recursive-include blag/static * +recursive-include blag/templates * diff --git a/Makefile b/Makefile index d973043..13e2bfd 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ VENV = venv BIN=$(VENV)/bin DOCS_SRC = docs -DOCS_OUT = $(DOCS_SRC)/_build +DOCS_OUT = site ifeq ($(OS), Windows_NT) @@ -14,40 +14,56 @@ endif .PHONY: all -all: lint test +all: lint mypy test test-release -$(VENV): requirements.txt requirements-dev.txt setup.py +$(VENV): requirements.txt requirements-dev.txt pyproject.toml $(PY) -m venv $(VENV) $(BIN)/pip install --upgrade -r requirements.txt $(BIN)/pip install --upgrade -r requirements-dev.txt - $(BIN)/pip install -e . + $(BIN)/pip install -e .['dev'] touch $(VENV) .PHONY: test test: $(VENV) $(BIN)/pytest +.PHONY: mypy +mypy: $(VENV) + $(BIN)/mypy + .PHONY: lint lint: $(VENV) - $(BIN)/flake8 + $(BIN)/ruff check . + +.PHONY: build +build: $(VENV) + rm -rf dist + $(BIN)/python3 -m build + +.PHONY: test-release +test-release: $(VENV) build + $(BIN)/twine check dist/* .PHONY: release -release: $(VENV) - rm -rf dist - $(BIN)/python setup.py sdist bdist_wheel +release: $(VENV) build $(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 docs: $(VENV) - $(BIN)/sphinx-build $(DOCS_SRC) $(DOCS_OUT) + $(BIN)/mkdocs build .PHONY: clean clean: rm -rf build dist *.egg-info rm -rf $(VENV) rm -rf $(DOCS_OUT) - rm -rf $(DOCS_SRC)/api find . -type f -name *.pyc -delete find . -type d -name __pycache__ -delete # coverage rm -rf htmlcov .coverage + rm -rf .mypy_cache diff --git a/README.md b/README.md index 77848f4..1038bb8 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ blag is named after [the blag of the webcomic xkcd][blagxkcd]. ## Features * Write content in [Markdown][] +* Good looking default theme: +  * Theming support using [Jinja2][] templates * Generation of Atom feeds for blog content * Fenced code blocks and syntax highlighting using [Pygments][] diff --git a/blag/__init__.py b/blag/__init__.py index 6527216..189ce64 100644 --- a/blag/__init__.py +++ b/blag/__init__.py @@ -1 +1 @@ -from blag.version import __VERSION__ # noqa +from blag.version import __VERSION__ as __VERSION__ # noqa diff --git a/blag/blag.py b/blag/blag.py index c47fc2b..2d72054 100644 --- a/blag/blag.py +++ b/blag/blag.py @@ -1,44 +1,47 @@ #!/usr/bin/env python3 -"""blag's core methods. +"""blag's core methods.""" -""" +# remove when we don't support py38 anymore +from __future__ import annotations import argparse +import configparser +import logging import os import shutil -import logging -import configparser import sys +from typing import Any -from jinja2 import Environment, ChoiceLoader, FileSystemLoader, PackageLoader import feedgenerator +from jinja2 import Environment, FileSystemLoader, Template, TemplateNotFound -from blag.markdown import markdown_factory, convert_markdown +import blag 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.version import __VERSION__ logger = logging.getLogger(__name__) logging.basicConfig( - level=logging.INFO, - format='%(asctime)s %(levelname)s %(name)s %(message)s', + level=logging.INFO, + format="%(asctime)s %(levelname)s %(name)s %(message)s", ) -def main(args=None): - """Main entrypoint for the CLI. +def main(arguments: list[str] | None = None) -> None: + """Run the CLI. This method parses the CLI arguments and executes the respective commands. Parameters ---------- - args : list[str] + arguments optional parameters, used for testing """ - args = parse_args(args) + args = parse_args(arguments) # set loglevel if args.verbose: logger.setLevel(logging.DEBUG) @@ -46,12 +49,12 @@ def main(args=None): args.func(args) -def parse_args(args=None): +def parse_args(args: list[str] | None = None) -> argparse.Namespace: """Parse command line arguments. Parameters ---------- - args : List[str] + args optional parameters, used for testing Returns @@ -61,142 +64,148 @@ def parse_args(args=None): """ parser = argparse.ArgumentParser() parser.add_argument( - '--version', - action='version', - version='%(prog)s '+__VERSION__, + "--version", + action="version", + version="%(prog)s " + __VERSION__, ) parser.add_argument( - '-v', '--verbose', - action='store_true', - help='Verbose output.', + "-v", + "--verbose", + action="store_true", + help="Verbose output.", ) - commands = parser.add_subparsers(dest='command') + commands = parser.add_subparsers(dest="command") commands.required = True build_parser = commands.add_parser( - 'build', - help='Build website.', + "build", + help="Build website.", ) build_parser.set_defaults(func=build) build_parser.add_argument( - '-i', '--input-dir', - default='content', - help='Input directory (default: content)', + "-i", + "--input-dir", + default="content", + help="Input directory (default: content)", ) build_parser.add_argument( - '-o', '--output-dir', - default='build', - help='Ouptut directory (default: build)', + "-o", + "--output-dir", + default="build", + help="Ouptut directory (default: build)", ) build_parser.add_argument( - '-t', '--template-dir', - default='templates', - help='Template directory (default: templates)', + "-t", + "--template-dir", + default="templates", + help="Template directory (default: templates)", ) build_parser.add_argument( - '-s', '--static-dir', - default='static', - help='Static directory (default: static)', + "-s", + "--static-dir", + default="static", + help="Static directory (default: static)", ) quickstart_parser = commands.add_parser( - 'quickstart', - help="Quickstart blag, creating necessary configuration.", + "quickstart", + help="Quickstart blag, creating necessary configuration.", ) quickstart_parser.set_defaults(func=quickstart) serve_parser = commands.add_parser( - 'serve', - help="Start development server.", + "serve", + help="Start development server.", ) serve_parser.set_defaults(func=serve) serve_parser.add_argument( - '-i', '--input-dir', - default='content', - help='Input directory (default: content)', + "-i", + "--input-dir", + default="content", + help="Input directory (default: content)", ) serve_parser.add_argument( - '-o', '--output-dir', - default='build', - help='Ouptut directory (default: build)', + "-o", + "--output-dir", + default="build", + help="Ouptut directory (default: build)", ) serve_parser.add_argument( - '-t', '--template-dir', - default='templates', - help='Template directory (default: templates)', + "-t", + "--template-dir", + default="templates", + help="Template directory (default: templates)", ) serve_parser.add_argument( - '-s', '--static-dir', - default='static', - help='Static directory (default: static)', + "-s", + "--static-dir", + default="static", + help="Static directory (default: static)", ) return parser.parse_args(args) -def get_config(configfile): +def get_config(configfile: str) -> configparser.SectionProxy: """Load site configuration from configfile. Parameters ---------- - configfile : str + configfile path to configuration file Returns ------- - dict + configparser.SectionProxy """ config = configparser.ConfigParser() config.read(configfile) # check for the mandatory options - for value in 'base_url', 'title', 'description', 'author': + for value in "base_url", "title", "description", "author": try: - config['main'][value] + config["main"][value] except Exception: - print(f'{value} is missing in {configfile}!') + print(f"{value} is missing in {configfile}!") sys.exit(1) - if not config['main']['base_url'].endswith('/'): - logger.warning('base_url does not end with a slash, adding it.') - config['main']['base_url'] += '/' + if not config["main"]["base_url"].endswith("/"): + logger.warning("base_url does not end with a slash, adding it.") + config["main"]["base_url"] += "/" - return config['main'] + return config["main"] -def environment_factory(template_dir=None, globals_=None): +def environment_factory( + template_dir: str, + globals_: dict[str, object] | None = None, +) -> Environment: """Environment factory. - Creates a Jinja2 Environment with the default templates and - additional templates from `template_dir` loaded. If `globals` are - provided, they are attached to the environment and thus available to - all contexts. + Creates a Jinja2 Environment with the templates from `template_dir` loaded. + If `globals` are provided, they are attached to the environment and thus + available to all contexts. Parameters ---------- - template_dir : str - globals_ : dict + template_dir + directory containing the templates + globals_ Returns ------- jinja2.Environment """ - # first we try the custom templates, and fall back the ones provided - # by blag - loaders = [] - if template_dir: - loaders.append(FileSystemLoader([template_dir])) - loaders.append(PackageLoader('blag', 'templates')) - env = Environment(loader=ChoiceLoader(loaders)) + env = Environment(loader=FileSystemLoader(template_dir)) if globals_: env.globals = globals_ return env -def build(args): +def build(args: argparse.Namespace) -> None: """Build the site. This is blag's main method that builds the site, generates the feed @@ -204,43 +213,57 @@ def build(args): Parameters ---------- - args : argparse.Namespace + args """ - os.makedirs(f'{args.output_dir}', exist_ok=True) + os.makedirs(f"{args.output_dir}", exist_ok=True) convertibles = [] for root, dirnames, filenames in os.walk(args.input_dir): for filename in filenames: - rel_src = os.path.relpath(f'{root}/{filename}', - start=args.input_dir) + rel_src = os.path.relpath( + f"{root}/{filename}", start=args.input_dir + ) # all non-markdown files are just copied over, the markdown # files are converted to html - if rel_src.endswith('.md'): + if rel_src.endswith(".md"): rel_dst = rel_src - rel_dst = rel_dst[:-3] + '.html' + rel_dst = rel_dst[:-3] + ".html" convertibles.append((rel_src, rel_dst)) else: - shutil.copy(f'{args.input_dir}/{rel_src}', - f'{args.output_dir}/{rel_src}') + shutil.copy( + f"{args.input_dir}/{rel_src}", + f"{args.output_dir}/{rel_src}", + ) for dirname in dirnames: # all directories are copied into the output directory - path = os.path.relpath(f'{root}/{dirname}', start=args.input_dir) - os.makedirs(f'{args.output_dir}/{path}', exist_ok=True) + path = os.path.relpath(f"{root}/{dirname}", start=args.input_dir) + os.makedirs(f"{args.output_dir}/{path}", exist_ok=True) # copy static files over - logger.info('Copying static files.') + logger.info("Copying static files.") if os.path.exists(args.static_dir): 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)) - page_template = env.get_template('page.html') - article_template = env.get_template('article.html') - archive_template = env.get_template('archive.html') - tags_template = env.get_template('tags.html') - tag_template = env.get_template('tag.html') + try: + page_template = env.get_template("page.html") + article_template = env.get_template("article.html") + index_template = env.get_template("index.html") + archive_template = env.get_template("archive.html") + tags_template = env.get_template("tags.html") + tag_template = env.get_template("tag.html") + except TemplateNotFound as exc: + tmpl = os.path.join(blag.__path__[0], "templates") + logger.error( + f'Template "{exc.name}" not found in {args.template_dir}! ' + "Consider running `blag quickstart` or copying the " + f"missing template from {tmpl}." + ) + + sys.exit(1) articles, pages = process_markdown( convertibles, @@ -251,18 +274,25 @@ def build(args): ) generate_feed( - articles, args.output_dir, - base_url=config['base_url'], - blog_title=config['title'], - blog_description=config['description'], - blog_author=config['author'], + articles, + args.output_dir, + base_url=config["base_url"], + blog_title=config["title"], + blog_description=config["description"], + blog_author=config["author"], ) + generate_index(articles, index_template, args.output_dir) generate_archive(articles, archive_template, args.output_dir) generate_tags(articles, tags_template, tag_template, args.output_dir) -def process_markdown(convertibles, input_dir, output_dir, - page_template, article_template): +def process_markdown( + convertibles: list[tuple[str, str]], + input_dir: str, + output_dir: str, + page_template: Template, + article_template: Template, +) -> tuple[list[tuple[str, dict[str, Any]]], list[tuple[str, dict[str, Any]]]]: """Process markdown files. This method processes the convertibles, converts them to html and @@ -271,18 +301,21 @@ def process_markdown(convertibles, input_dir, output_dir, If a markdown file has a `date` metadata field it will be recognized as article otherwise as page. + Articles are sorted by date in descending order. + Parameters ---------- - convertibles : List[Tuple[str, str]] + convertibles relative paths to markdown- (src) html- (dest) files - input_dir : str - output_dir : str - page_template, archive_template : jinja2 template - templats for pages and articles + input_dir + output_dir + page_template, archive_template + templates for pages and articles Returns ------- - articles, pages : List[Tuple[str, Dict]] + list[tuple[str, dict[str, Any]]], list[tuple[str, dict[str, Any]]] + articles and pages, articles are sorted by date in descending order. """ logger.info("Converting Markdown files...") @@ -291,21 +324,21 @@ def process_markdown(convertibles, input_dir, output_dir, articles = [] pages = [] for src, dst in convertibles: - logger.debug(f'Processing {src}') + logger.debug(f"Processing {src}") # see first if the dst actually needs re-building. for that we compare # the mtimes and assume mtime_dst > mtime_src means that it needs not # to be rebuilt - if os.path.exists(f'{output_dir}/{dst}'): - mtime_src = os.stat(f'{input_dir}/{src}').st_mtime - mtime_dst = os.stat(f'{output_dir}/{dst}').st_mtime + if os.path.exists(f"{output_dir}/{dst}"): + mtime_src = os.stat(f"{input_dir}/{src}").st_mtime + mtime_dst = os.stat(f"{output_dir}/{dst}").st_mtime if mtime_dst >= mtime_src: logger.debug( - 'Skipping, as target exists and is newer than source.' + "Skipping, as target exists and is newer than source." ) continue - with open(f'{input_dir}/{src}', 'r') as fh: + with open(f"{input_dir}/{src}") as fh: body = fh.read() content, meta = convert_markdown(md, body) @@ -315,139 +348,180 @@ def process_markdown(convertibles, input_dir, output_dir, # if markdown has date in meta, we treat it as a blog article, # everything else are just pages - if meta and 'date' in meta: + if meta and "date" in meta: articles.append((dst, context)) result = article_template.render(context) else: pages.append((dst, 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) # 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 def generate_feed( - articles, - output_dir, - base_url, - blog_title, - blog_description, - blog_author, -): + articles: list[tuple[str, dict[str, Any]]], + output_dir: str, + base_url: str, + blog_title: str, + blog_description: str, + blog_author: str, +) -> None: """Generate Atom feed. Parameters ---------- - articles : list[list[str, dict]] + articles list of relative output path and article dictionary - output_dir : str + output_dir where the feed is stored - base_url : str + base_url base url - blog_title : str + blog_title blog title - blog_description : str + blog_description blog description - blog_author : str + blog_author blog author """ - logger.info('Generating Atom feed.') + logger.info("Generating Atom feed.") feed = feedgenerator.Atom1Feed( - link=base_url, - title=blog_title, - description=blog_description, - feed_url=base_url + 'atom.xml', + link=base_url, + title=blog_title, + description=blog_description, + feed_url=base_url + "atom.xml", ) for dst, context in articles: # if article has a description, use that. otherwise fall back to # the title - description = context.get('description', context['title']) + description = context.get("description", context["title"]) feed.add_item( - title=context['title'], + title=context["title"], author_name=blog_author, link=base_url + dst, description=description, - content=context['content'], - pubdate=context['date'], + content=context["content"], + pubdate=context["date"], ) - with open(f'{output_dir}/atom.xml', 'w') as fh: - feed.write(fh, encoding='utf8') + with open(f"{output_dir}/atom.xml", "w") as fh: + feed.write(fh, encoding="utf8") -def generate_archive(articles, template, output_dir): - """Generate the archive page. +def generate_index( + articles: list[tuple[str, dict[str, Any]]], + template: Template, + output_dir: str, +) -> None: + """Generate the index page. + + This is used for the index (i.e. landing) page. Parameters ---------- - articles : list[list[str, dict]] + articles List of articles. Each article has the destination path and a dictionary with the content. - template : jinja2.Template instance - output_dir : str + template + output_dir """ archive = [] for dst, context in articles: entry = context.copy() - entry['dst'] = dst + entry["dst"] = dst archive.append(entry) 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_tags(articles, tags_template, tag_template, output_dir): +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) + + +def generate_tags( + articles: list[tuple[str, dict[str, Any]]], + tags_template: Template, + tag_template: Template, + output_dir: str, +) -> None: """Generate the tags page. Parameters ---------- - articles : list[list[str, dict]] + articles List of articles. Each article has the destination path and a dictionary with the content. - tags_template, tag_template : jinja2.Template instance - output_dir : str + tags_template, tag_template + output_dir """ 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 - all_tags = {} + all_tags: dict[str, int] = {} for _, context in articles: - tags = context.get('tags', []) + tags: list[str] = context.get("tags", []) for tag in tags: all_tags[tag] = all_tags.get(tag, 0) + 1 # sort by occurrence - all_tags = sorted(all_tags.items(), key=lambda x: x[1], reverse=True) + taglist: list[tuple[str, int]] = sorted( + all_tags.items(), key=lambda x: x[1], reverse=True + ) - result = tags_template.render(dict(tags=all_tags)) - with open(f'{output_dir}/tags/index.html', 'w') as fh: + result = tags_template.render(dict(tags=taglist)) + with open(f"{output_dir}/tags/index.html", "w") as fh: fh.write(result) # get tags and archive per tag - all_tags = {} + all_tags2: dict[str, list[dict[str, Any]]] = {} for dst, context in articles: - tags = context.get('tags', []) + tags = context.get("tags", []) for tag in tags: - archive = all_tags.get(tag, []) + archive: list[dict[str, Any]] = all_tags2.get(tag, []) entry = context.copy() - entry['dst'] = dst + entry["dst"] = dst archive.append(entry) - all_tags[tag] = archive + all_tags2[tag] = archive - for tag, archive in all_tags.items(): + for tag, archive in all_tags2.items(): 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) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/blag/content/about.md b/blag/content/about.md new file mode 100644 index 0000000..4f01ccf --- /dev/null +++ b/blag/content/about.md @@ -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). diff --git a/blag/content/hello-world.md b/blag/content/hello-world.md new file mode 100644 index 0000000..7039dd9 --- /dev/null +++ b/blag/content/hello-world.md @@ -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) diff --git a/blag/content/second-post.md b/blag/content/second-post.md new file mode 100644 index 0000000..354fac5 --- /dev/null +++ b/blag/content/second-post.md @@ -0,0 +1,11 @@ +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 :) + + diff --git a/blag/content/testpage.md b/blag/content/testpage.md new file mode 100644 index 0000000..24fe02b --- /dev/null +++ b/blag/content/testpage.md @@ -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 +``` diff --git a/blag/devserver.py b/blag/devserver.py index dbc7386..4f3ec50 100644 --- a/blag/devserver.py +++ b/blag/devserver.py @@ -6,20 +6,24 @@ site if necessary. """ -import os +# remove when we don't support py38 anymore +from __future__ import annotations + +import argparse import logging -import time import multiprocessing -from http.server import SimpleHTTPRequestHandler, HTTPServer +import os +import time from functools import partial +from http.server import HTTPServer, SimpleHTTPRequestHandler +from typing import NoReturn from blag import blag - logger = logging.getLogger(__name__) -def get_last_modified(dirs): +def get_last_modified(dirs: list[str]) -> float: """Get the last modified time. This method recursively goes through `dirs` and returns the most @@ -27,16 +31,16 @@ def get_last_modified(dirs): Parameters ---------- - dirs : list[str] + dirs list of directories to search Returns ------- - int + float most recent modification time found in `dirs` """ - last_mtime = 0 + last_mtime = 0.0 for dir in dirs: for root, dirs, files in os.walk(dir): @@ -48,7 +52,7 @@ def get_last_modified(dirs): return last_mtime -def autoreload(args): +def autoreload(args: argparse.Namespace) -> NoReturn: """Start the autoreloader. This method monitors the given directories for changes (i.e. the @@ -60,33 +64,37 @@ def autoreload(args): Parameters ---------- - args : argparse.Namespace + args + contains the input-, template- and 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 # loop to avoid serving stale contents - last_mtime = 0 + last_mtime = 0.0 while True: mtime = get_last_modified(dirs) if mtime > last_mtime: last_mtime = mtime - logger.info('Change detected, rebuilding...') + logger.info("Change detected, rebuilding...") blag.build(args) time.sleep(1) -def serve(args): +def serve(args: argparse.Namespace) -> None: """Start the webserver and the autoreloader. Parameters ---------- - args : arparse.Namespace + args + contains the input-, template- and static dir """ - httpd = HTTPServer(('', 8000), partial(SimpleHTTPRequestHandler, - directory=args.output_dir)) + httpd = HTTPServer( + ("", 8000), + partial(SimpleHTTPRequestHandler, directory=args.output_dir), + ) proc = multiprocessing.Process(target=autoreload, args=(args,)) proc.start() logger.info("\n\n Devserver Started -- visit http://localhost:8000\n") diff --git a/blag/markdown.py b/blag/markdown.py index 328b0d0..bc7aa6b 100644 --- a/blag/markdown.py +++ b/blag/markdown.py @@ -5,19 +5,22 @@ processing. """ -from datetime import datetime +# remove when we don't support py38 anymore +from __future__ import annotations + import logging +from datetime import datetime from urllib.parse import urlsplit, urlunsplit +from xml.etree.ElementTree import Element from markdown import Markdown from markdown.extensions import Extension from markdown.treeprocessors import Treeprocessor - logger = logging.getLogger(__name__) -def markdown_factory(): +def markdown_factory() -> Markdown: """Create a Markdown instance. This method exists only to ensure we use the same Markdown instance @@ -30,15 +33,21 @@ def markdown_factory(): """ md = Markdown( extensions=[ - 'meta', 'fenced_code', 'codehilite', 'smarty', - MarkdownLinkExtension() + "meta", + "fenced_code", + "codehilite", + "smarty", + MarkdownLinkExtension(), ], - output_format='html5', + output_format="html", ) return md -def convert_markdown(md, markdown): +def convert_markdown( + md: Markdown, + markdown: str, +) -> tuple[str, dict[str, str]]: """Convert markdown into html and extract meta data. Some meta data is treated special: @@ -48,72 +57,80 @@ def convert_markdown(md, markdown): Parameters ---------- - md : markdown.Markdown instance - markdown : str + md + the Markdown instance + markdown + the markdown text that should be converted Returns ------- - str, dict : + str, dict[str, str] html and metadata """ md.reset() content = md.convert(markdown) - meta = md.Meta + meta = md.Meta # type: ignore # markdowns metadata consists as list of strings -- one item per # line. let's convert into single strings. for key, value in meta.items(): - value = '\n'.join(value) + value = "\n".join(value) meta[key] = value # convert known metadata # date: datetime - if 'date' in meta: - meta['date'] = datetime.fromisoformat(meta['date']) - meta['date'] = meta['date'].astimezone() + if "date" in meta: + meta["date"] = datetime.fromisoformat(meta["date"]) + meta["date"] = meta["date"].astimezone() # tags: list[str] and lower case - if 'tags' in meta: - tags = meta['tags'].split(',') + if "tags" in meta: + tags = meta["tags"].split(",") tags = [t.lower() for t in tags] tags = [t.strip() for t in tags] - meta['tags'] = tags + meta["tags"] = tags return content, meta class MarkdownLinkTreeprocessor(Treeprocessor): - """Converts relative links to .md files to .html + """Converts relative links to .md files to .html.""" - """ - - def run(self, root): + def run(self, root: Element) -> Element: + """Process the ElementTree.""" for element in root.iter(): - if element.tag == 'a': - url = element.get('href') + if element.tag == "a": + url = element.get("href") + # element.get could also return None, we haven't seen this so + # far, so lets wait if we raise this + assert url is not None + url = str(url) converted = self.convert(url) - element.set('href', converted) + element.set("href", converted) return root - def convert(self, url): + def convert(self, url: str) -> str: + """Convert relative .md-links to .html-links.""" scheme, netloc, path, query, fragment = urlsplit(url) 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 - if path.endswith('.md'): - path = path[:-3] + '.html' + if path.endswith(".md"): + path = path[:-3] + ".html" url = urlunsplit((scheme, netloc, path, query, fragment)) return url class MarkdownLinkExtension(Extension): - """markdown.extension that converts relative .md- to .html-links. + """markdown.extension that converts relative .md- to .html-links.""" - """ - def extendMarkdown(self, md): + def extendMarkdown(self, md: Markdown) -> None: + """Register the MarkdownLinkTreeprocessor.""" md.treeprocessors.register( - MarkdownLinkTreeprocessor(md), 'mdlink', 0, + MarkdownLinkTreeprocessor(md), + "mdlink", + 0, ) diff --git a/blag/quickstart.py b/blag/quickstart.py index 00b4725..fdd34d8 100644 --- a/blag/quickstart.py +++ b/blag/quickstart.py @@ -1,11 +1,17 @@ -"""Helper methods for blag's quickstart command. +"""Helper methods for blag's quickstart command.""" -""" +# remove when we don't support py38 anymore +from __future__ import annotations +import argparse import configparser +import os +import shutil + +import blag -def get_input(question, default): +def get_input(question: str, default: str) -> str: """Prompt for user input. This is a wrapper around the input-builtin. It will show the default answer @@ -13,14 +19,15 @@ def get_input(question, default): Parameters ---------- - question : str + question the question the user is presented - default : str + default the default value that will be used if no answer was given Returns ------- str + the answer """ reply = input(f"{question} [{default}]: ") @@ -29,15 +36,38 @@ def get_input(question, default): return reply -def quickstart(args): +def copy_default_theme() -> None: + """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. + + """ + print("Copying default theme...") + for dir_ in "templates", "content", "static": + print(f" Copying {dir_}...") + try: + shutil.copytree( + os.path.join(blag.__path__[0], dir_), + dir_, + ) + except FileExistsError: + print(f" {dir_} already exist. Skipping.") + + +def quickstart(args: argparse.Namespace | None) -> None: """Quickstart. - This method asks the user some questions and generates a - configuration file that is needed in order to run blag. + This method asks the user some questions and generates a configuration file + 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 ---------- - args : argparse.Namespace + args + not used """ base_url = get_input( @@ -58,11 +88,13 @@ def quickstart(args): ) config = configparser.ConfigParser() - config['main'] = { - 'base_url': base_url, - 'title': title, - 'description': description, - 'author': author, + config["main"] = { + "base_url": base_url, + "title": title, + "description": description, + "author": author, } - with open('config.ini', 'w') as fh: + with open("config.ini", "w") as fh: config.write(fh) + + copy_default_theme() diff --git a/blag/static/blag.png b/blag/static/blag.png new file mode 100644 index 0000000..06445a2 Binary files /dev/null and b/blag/static/blag.png differ diff --git a/blag/static/code-dark.css b/blag/static/code-dark.css new file mode 100644 index 0000000..a2af57e --- /dev/null +++ b/blag/static/code-dark.css @@ -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 */ diff --git a/blag/static/code-light.css b/blag/static/code-light.css new file mode 100644 index 0000000..e2cc7b8 --- /dev/null +++ b/blag/static/code-light.css @@ -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 */ diff --git a/blag/static/favicon.ico b/blag/static/favicon.ico new file mode 100644 index 0000000..6175ffc Binary files /dev/null and b/blag/static/favicon.ico differ diff --git a/blag/static/style.css b/blag/static/style.css new file mode 100644 index 0000000..f00afd6 --- /dev/null +++ b/blag/static/style.css @@ -0,0 +1,170 @@ +@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); +} + +img { + max-width: 100%; +} + +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; +} diff --git a/blag/templates/archive.html b/blag/templates/archive.html index e8f9cca..5109438 100644 --- a/blag/templates/archive.html +++ b/blag/templates/archive.html @@ -1,20 +1,23 @@ {% extends "base.html" %} -{% block title %}{{ site.title }}{% endblock %} +{% block title %}Archive{% endblock %} {% block content %} + {% for entry in archive %} - {% if entry.title %} -
— {{ entry.description }}
- {% endif %} - - {% endif %} - -Written on {{ entry.date.date() }}.
+— {{ entry.description }}
+ {% endif %} +