forked from github.com/blag
Compare commits
211 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8656780e5b | ||
|
|
ff1099de60 | ||
|
|
a0374a777e | ||
|
|
55eb9d7482 | ||
|
|
55e2f41b88 | ||
|
|
8c61614295 | ||
|
|
c364087eab | ||
|
|
a06947d762 | ||
|
|
12a7fb9568 | ||
|
|
caf6217221 | ||
|
|
744dadb1a5 | ||
|
|
7defacd3f6 | ||
|
|
c1d741a56c | ||
|
|
056ddbdbcb | ||
|
|
9b0bd2814a | ||
|
|
9378bb5103 | ||
|
|
8f287c5b8d | ||
|
|
77954f13ef | ||
|
|
4b5ca7a414 | ||
|
|
6ec70ba61b | ||
|
|
082f1e0473 | ||
|
|
b64563757c | ||
|
|
dfcbaec6f9 | ||
|
|
5314a7d260 | ||
|
|
f507693f05 | ||
|
|
86356a5e1d | ||
|
|
36be7fcc07 | ||
|
|
869ca2a95e | ||
|
|
9734386397 | ||
|
|
d66b2955a5 | ||
|
|
b889bb5b85 | ||
|
|
2a6c7d0b0e | ||
|
|
95546b7dab | ||
|
|
d6e049bebb | ||
|
|
e13a2cb50b | ||
|
|
ed33fdb542 | ||
|
|
7d7723219b | ||
|
|
a550d7b194 | ||
|
|
096d86e339 | ||
|
|
81a64019ee | ||
|
|
b3ad97add7 | ||
|
|
b0b0f6fbbf | ||
|
|
a416095cd0 | ||
|
|
65e5fd8728 | ||
|
|
557077e646 | ||
|
|
f2900fdb4b | ||
|
|
a7c221f345 | ||
|
|
c92130559c | ||
|
|
4f3516fb19 | ||
|
|
ead8c518c3 | ||
|
|
6431ffae8c | ||
|
|
33b5c282f9 | ||
|
|
3b12986dc9 | ||
|
|
91c6126e4f | ||
|
|
ca5405f7aa | ||
|
|
f0e6cd2144 | ||
|
|
81febfbbd1 | ||
|
|
e8af1d0380 | ||
|
|
c9f7242f02 | ||
|
|
ceb6de574f | ||
|
|
4382dff703 | ||
|
|
cfe7f36868 | ||
|
|
c45fb44c9f | ||
|
|
f09b30d069 | ||
|
|
d836025594 | ||
|
|
ea345c6464 | ||
|
|
31b2a6d592 | ||
|
|
ad41be706d | ||
|
|
04b022276c | ||
|
|
9c1f93d075 | ||
|
|
0276f6b328 | ||
|
|
a3637621d7 | ||
|
|
392181e728 | ||
|
|
b44c303d7a | ||
|
|
90f8296a62 | ||
|
|
298a828ecd | ||
|
|
9acbc77fea | ||
|
|
90437f05ab | ||
|
|
33ae0e4482 | ||
|
|
37c5a4eca6 | ||
|
|
7b14d7dcbd | ||
|
|
35d5ab93cd | ||
|
|
8ba56b6b27 | ||
|
|
df84fbb96a | ||
|
|
609b49b6f2 | ||
|
|
34d33d380c | ||
|
|
c1b16e9a82 | ||
|
|
db1d81713c | ||
|
|
ee3d289ea0 | ||
|
|
75b926de67 | ||
|
|
0b7e898f3a | ||
|
|
acce7d1c32 | ||
|
|
e4786eca74 | ||
|
|
62f36cdd05 | ||
|
|
111957883e | ||
|
|
a1cfe01373 | ||
|
|
3cfd756ac1 | ||
|
|
32fff6dabb | ||
|
|
86826c0c03 | ||
|
|
f646c89152 | ||
|
|
1db80df0ce | ||
|
|
2c15b2c2dd | ||
|
|
44d5c4d694 | ||
|
|
1ebae15cda | ||
|
|
175812e18c | ||
|
|
08af02b9d1 | ||
|
|
a6e0a7168c | ||
|
|
da364efd6c | ||
|
|
d918e8e0f9 | ||
|
|
75b7878780 | ||
|
|
86868c7c52 | ||
|
|
92ad157e31 | ||
|
|
b90a54d094 | ||
|
|
76b06b31f9 | ||
|
|
0349bd3359 | ||
|
|
f6c5eaf375 | ||
|
|
19b41f73f8 | ||
|
|
d6ed2d71d5 | ||
|
|
7e22f00ac5 | ||
|
|
6708c6c80a | ||
|
|
ace1104df0 | ||
|
|
61cdae6b56 | ||
|
|
6b928e4953 | ||
|
|
cde5b53298 | ||
|
|
fe268516e3 | ||
|
|
e1b1ebde32 | ||
|
|
f97e641bd8 | ||
|
|
c952c574b7 | ||
|
|
362e721d88 | ||
|
|
a3572d414f | ||
|
|
128d3f032d | ||
|
|
e765d6b717 | ||
|
|
d8ff4afddb | ||
|
|
169f0f2e0b | ||
|
|
6fcbb2e1f6 | ||
|
|
ddfee7228c | ||
|
|
f2facf895e | ||
|
|
0e2847ca61 | ||
|
|
996b51e07a | ||
|
|
322957e98f | ||
|
|
06d8623dd7 | ||
|
|
c3f0dffd11 | ||
|
|
f6c18d3819 | ||
|
|
fe43288c8c | ||
|
|
352c37045a | ||
|
|
0ca248ede3 | ||
|
|
6243552de1 | ||
|
|
a330d9766d | ||
|
|
33728ce713 | ||
|
|
4c41d1429f | ||
|
|
f1e122cd23 | ||
|
|
fcfa1bf06e | ||
|
|
470bfba2bf | ||
|
|
5902582c47 | ||
|
|
e168a0e0d3 | ||
|
|
c3993f5111 | ||
|
|
ffe9d21f91 | ||
|
|
d8d2f4f5d6 | ||
|
|
58b57839a6 | ||
|
|
ca8db66d52 | ||
|
|
837f9e1026 | ||
|
|
bf368c0ce6 | ||
|
|
e45e59d568 | ||
|
|
642e31c357 | ||
|
|
e317f80d1c | ||
|
|
a960db4952 | ||
|
|
4b2f88a68a | ||
|
|
153b407697 | ||
|
|
34eb413434 | ||
|
|
629a3aa473 | ||
|
|
c06424cfdf | ||
|
|
31fc3d688b | ||
|
|
2d4f3334cf | ||
|
|
118b20ef33 | ||
|
|
a4aa8045eb | ||
|
|
73672598e5 | ||
|
|
fc4eb0f463 | ||
|
|
a224840b28 | ||
|
|
0399961aa3 | ||
|
|
e2568d715e | ||
|
|
01507e9de6 | ||
|
|
87d619cc1c | ||
|
|
7b6b219cdf | ||
|
|
875fd85d65 | ||
|
|
f59a648779 | ||
|
|
ebac0a8fc4 | ||
|
|
2adc7b3bd4 | ||
|
|
10f84ebb16 | ||
|
|
451fb1b260 | ||
|
|
322154041a | ||
|
|
f8cd915ac2 | ||
|
|
ad0ab1a0fe | ||
|
|
641f0ed94e | ||
|
|
6e94a0c094 | ||
|
|
7587fa2cad | ||
|
|
91cd948c4b | ||
|
|
bb3101ad77 | ||
|
|
9397e4c287 | ||
|
|
d1de9692ea | ||
|
|
c5ad4757e7 | ||
|
|
9e07e2e100 | ||
|
|
10bac5531f | ||
|
|
6d82d2ab79 | ||
|
|
595356e915 | ||
|
|
138a78357a | ||
|
|
6005369108 | ||
|
|
db4e03afde | ||
|
|
877c47c391 | ||
|
|
3bd7125873 | ||
|
|
35f6ef05b6 | ||
|
|
1e74596101 |
5
.github/dependabot.yml
vendored
5
.github/dependabot.yml
vendored
@@ -4,3 +4,8 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
60
.github/workflows/python-package.yaml
vendored
60
.github/workflows/python-package.yaml
vendored
@@ -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
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,10 +5,10 @@ build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
|
||||
docs/_build/
|
||||
docs/api/
|
||||
site/
|
||||
|
||||
htmlcov/
|
||||
.coverage
|
||||
.mypy_cache
|
||||
|
||||
venv/
|
||||
|
||||
@@ -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
|
||||
|
||||
121
CHANGELOG.md
121
CHANGELOG.md
@@ -1,5 +1,126 @@
|
||||
# Changelog
|
||||
|
||||
## [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
|
||||
* pygments 2.13.0
|
||||
* flake 5.0.4
|
||||
* twine 4.0.1
|
||||
* sphinx 5.1.1
|
||||
|
||||
## [1.3.2] - 2022-06-29
|
||||
|
||||
* Added --version option
|
||||
* added --verbose option, that increases the loglevel to 'debug'
|
||||
* Improved quickstart:
|
||||
* respective default answers will be written to config if user provided no
|
||||
answer
|
||||
* added tests for quickstart
|
||||
* Added some test cases for the MarkdownLinktreeProcessor
|
||||
|
||||
## [1.3.1] - 2022-06-10
|
||||
|
||||
* fixed man page
|
||||
|
||||
3
MANIFEST.in
Normal file
3
MANIFEST.in
Normal file
@@ -0,0 +1,3 @@
|
||||
recursive-include blag/content *
|
||||
recursive-include blag/static *
|
||||
recursive-include blag/templates *
|
||||
36
Makefile
36
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
|
||||
|
||||
@@ -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][]
|
||||
|
||||
@@ -1 +1 @@
|
||||
from blag.version import __VERSION__ # noqa
|
||||
from blag.version import __VERSION__ as __VERSION__ # noqa
|
||||
|
||||
458
blag/blag.py
458
blag/blag.py
@@ -1,51 +1,61 @@
|
||||
#!/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 sqlite3
|
||||
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.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)
|
||||
logger.debug(f"This is blag {__VERSION__}.")
|
||||
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
|
||||
@@ -54,133 +64,149 @@ def parse_args(args=None):
|
||||
|
||||
"""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
action="version",
|
||||
version="%(prog)s " + __VERSION__,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-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
|
||||
@@ -188,42 +214,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.")
|
||||
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,
|
||||
@@ -234,18 +275,27 @@ 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)
|
||||
generate_search(articles, pages, 'corpus.db')
|
||||
logger.info("Done.")
|
||||
|
||||
|
||||
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
|
||||
@@ -254,18 +304,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...")
|
||||
@@ -274,8 +327,9 @@ def process_markdown(convertibles, input_dir, output_dir,
|
||||
articles = []
|
||||
pages = []
|
||||
for src, dst in convertibles:
|
||||
logger.info(f'Processing {src}')
|
||||
with open(f'{input_dir}/{src}', 'r') as fh:
|
||||
logger.debug(f"Processing {src}")
|
||||
|
||||
with open(f"{input_dir}/{src}") as fh:
|
||||
body = fh.read()
|
||||
|
||||
content, meta = convert_markdown(md, body)
|
||||
@@ -285,167 +339,223 @@ 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)
|
||||
|
||||
|
||||
def quickstart(args):
|
||||
"""Quickstart.
|
||||
|
||||
This method asks the user some questions and generates a
|
||||
configuration file that is needed in order to run blag.
|
||||
def generate_search(
|
||||
articles: list[tuple[str, dict[str, Any]]],
|
||||
pages: list[tuple[str, dict[str, Any]]],
|
||||
db: str,
|
||||
) -> None:
|
||||
"""Generate Search.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
args : argparse.Namespace
|
||||
articles, pages
|
||||
db
|
||||
path to sqlite file
|
||||
|
||||
"""
|
||||
base_url = input("Hostname (and path) to the root? "
|
||||
"[https://example.com/]: ")
|
||||
title = input("Title of your website? ")
|
||||
description = input("Description of your website [John Doe's Blog]? ")
|
||||
author = input("Author of your website [John Doe]? ")
|
||||
logger.info("Generating full text search.")
|
||||
conn = sqlite3.connect(db)
|
||||
with conn:
|
||||
conn.executescript("""
|
||||
drop table if exists corpus;
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config['main'] = {
|
||||
'base_url': base_url,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'author': author,
|
||||
}
|
||||
with open('config.ini', 'w') as fh:
|
||||
config.write(fh)
|
||||
create virtual table corpus using fts5(
|
||||
link,
|
||||
text,
|
||||
tokenize = porter
|
||||
);
|
||||
""")
|
||||
|
||||
with conn:
|
||||
for dst, context in articles:
|
||||
text = context['content']
|
||||
conn.execute("""
|
||||
insert into corpus(link, text)
|
||||
values(:link, :text)
|
||||
""", dict(link=dst, text=text))
|
||||
|
||||
for dst, context in pages:
|
||||
text = context['content']
|
||||
conn.execute("""
|
||||
insert into corpus(link, text)
|
||||
values(:link, :text)
|
||||
""", dict(link=dst, text=text))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__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)
|
||||
11
blag/content/second-post.md
Normal file
11
blag/content/second-post.md
Normal file
@@ -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 :)
|
||||
|
||||

|
||||
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
|
||||
```
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
100
blag/quickstart.py
Normal file
100
blag/quickstart.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""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: str, default: str) -> str:
|
||||
"""Prompt for user input.
|
||||
|
||||
This is a wrapper around the input-builtin. It will show the default answer
|
||||
in the prompt and -- if no answer was given -- use the default.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
question
|
||||
the question the user is presented
|
||||
default
|
||||
the default value that will be used if no answer was given
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
the answer
|
||||
|
||||
"""
|
||||
reply = input(f"{question} [{default}]: ")
|
||||
if not reply:
|
||||
reply = default
|
||||
return reply
|
||||
|
||||
|
||||
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. Additionally, it creates the content
|
||||
and static directories with some initial content, to get the user started.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
args
|
||||
not used
|
||||
|
||||
"""
|
||||
base_url = get_input(
|
||||
"Hostname (and path) to the root?",
|
||||
"https://example.com/",
|
||||
)
|
||||
title = get_input(
|
||||
"Title of your website?",
|
||||
"My little blog",
|
||||
)
|
||||
description = get_input(
|
||||
"Description of your website?",
|
||||
"John Doe's Blog",
|
||||
)
|
||||
author = get_input(
|
||||
"Author of your website",
|
||||
"John Doe",
|
||||
)
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config["main"] = {
|
||||
"base_url": base_url,
|
||||
"title": title,
|
||||
"description": description,
|
||||
"author": author,
|
||||
}
|
||||
with open("config.ini", "w") as fh:
|
||||
config.write(fh)
|
||||
|
||||
copy_default_theme()
|
||||
BIN
blag/static/blag.png
Normal file
BIN
blag/static/blag.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
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 |
170
blag/static/style.css
Normal file
170
blag/static/style.css
Normal file
@@ -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;
|
||||
}
|
||||
@@ -1,15 +1,23 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ site.title }}{% endblock %}
|
||||
{% block title %}Archive{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% for entry in archive %}
|
||||
|
||||
{% if entry.title %}
|
||||
<h1><a href="{{entry.dst}}">{{entry.title}}</a></h1>
|
||||
{% endif %}
|
||||
|
||||
<p>Written on {{ entry.date.date() }}.</p>
|
||||
<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 %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,5 +3,25 @@
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ content }}
|
||||
|
||||
{% if title %}
|
||||
<h2>{{ title }}</h2>
|
||||
{% endif %}
|
||||
|
||||
<aside>
|
||||
<p>published on {{ date.date() }}
|
||||
|
||||
{% if tags %}
|
||||
· tagged with
|
||||
{% for tag in tags|sort(case_sensitive=true) %}
|
||||
{%- if not loop.first and not loop.last %}, {% endif -%}
|
||||
{%- if loop.last and not loop.first %} and {% endif %}
|
||||
<a href="/tags/{{ tag }}.html">#{{ tag }}</a>
|
||||
{%- endfor %}
|
||||
{% endif %}
|
||||
</p>
|
||||
</aside>
|
||||
|
||||
{{ content }}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,20 +4,28 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="author" content="{{ site.author }}">
|
||||
{%- if description %}
|
||||
<meta name="description" content="{{ description }}">
|
||||
{% endif %}
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
{%- else %}
|
||||
<meta name="description" content="{{ site.description }}">
|
||||
{%- 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>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<h1>A Blog</h1>
|
||||
<h1><a href="/">{{ site.title }}</a></h1>
|
||||
<nav>
|
||||
<h2>{{ site.description }}</h2>
|
||||
<ul>
|
||||
<li><a href="/">Blog</a></li>
|
||||
<li><a href="/atom.xml">Atom Feed</a></li>
|
||||
<li><h2><a href="/">Blog</a></h2></li>
|
||||
<li><h2><a href="/archive.html">Archive</a></h2></li>
|
||||
<li><h2><a href="/tags/">Tags</a></h2></li>
|
||||
<li><h2><a href="/about.html">About Me</a></h2></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
@@ -26,7 +34,17 @@
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</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>
|
||||
|
||||
</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 content %}
|
||||
{{ content }}
|
||||
|
||||
{{ content }}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Tag {{ tag }}{% endblock %}
|
||||
{% block title %}#{{ tag }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h2>Articles tagged "{{ tag }}"</h2>
|
||||
|
||||
{% for entry in archive %}
|
||||
|
||||
{% if entry.title %}
|
||||
<h1><a href="/{{entry.dst}}">{{entry.title}}</a></h1>
|
||||
{% endif %}
|
||||
|
||||
<p>Written on {{ entry.date.date() }}.</p>
|
||||
<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 %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
__VERSION__ = '1.3.1'
|
||||
"""Version information for the blag package."""
|
||||
|
||||
__VERSION__ = "2.2.1"
|
||||
|
||||
2
debian/blag-doc.docs
vendored
2
debian/blag-doc.docs
vendored
@@ -1 +1 @@
|
||||
build/html/
|
||||
site/
|
||||
|
||||
1
debian/blag.install
vendored
1
debian/blag.install
vendored
@@ -1 +0,0 @@
|
||||
build/man/blag.1 /usr/share/man/man1
|
||||
115
debian/changelog
vendored
115
debian/changelog
vendored
@@ -1,3 +1,118 @@
|
||||
blag (2.2.1) unstable; urgency=medium
|
||||
|
||||
* fixed suggests field to blag-doc (Closes: #1055769)
|
||||
|
||||
-- Bastian Venthur <venthur@debian.org> Sat, 11 Nov 2023 10:57:06 +0100
|
||||
|
||||
blag (2.2.0) unstable; urgency=medium
|
||||
|
||||
* switched from flake8 to ruff
|
||||
* added missing docstrings
|
||||
* fixed dev requirements in pyproject, still pointing to sphinx
|
||||
* added Python3.12 to test suite
|
||||
* removed watch file again
|
||||
|
||||
-- Bastian Venthur <venthur@debian.org> Sun, 05 Nov 2023 17:08:09 +0100
|
||||
|
||||
blag (2.1.0) unstable; urgency=medium
|
||||
|
||||
* 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
|
||||
|
||||
-- Bastian Venthur <venthur@debian.org> Sun, 27 Aug 2023 15:27:39 +0200
|
||||
|
||||
blag (2.0.0) unstable; urgency=medium
|
||||
|
||||
* new upstream version
|
||||
|
||||
* 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
|
||||
|
||||
* Changes:
|
||||
* 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 pyproject.toml to include tests/conftest.py
|
||||
|
||||
-- Bastian Venthur <venthur@debian.org> Fri, 16 Jun 2023 22:34:29 +0200
|
||||
|
||||
blag (1.5.0) unstable; urgency=medium
|
||||
|
||||
* new upstream version
|
||||
* 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
|
||||
|
||||
-- Bastian Venthur <venthur@debian.org> Sun, 16 Apr 2023 10:48:18 +0200
|
||||
|
||||
blag (1.4.1) unstable; urgency=medium
|
||||
|
||||
* Applied multi-arch fix by debian-janitor
|
||||
|
||||
-- Bastian Venthur <venthur@debian.org> Thu, 29 Sep 2022 20:41:28 +0200
|
||||
|
||||
blag (1.4.0) unstable; urgency=medium
|
||||
|
||||
* added type hints and mypy --strict to test suite
|
||||
* improved default template
|
||||
|
||||
-- Bastian Venthur <venthur@debian.org> Thu, 01 Sep 2022 18:59:11 +0200
|
||||
|
||||
blag (1.3.2) unstable; urgency=medium
|
||||
|
||||
* Added --version option
|
||||
* Improved quickstart:
|
||||
* respective default answers will be written to config if user provided no
|
||||
answer
|
||||
* added tests for quickstart
|
||||
* Added some test cases for the MarkdownLinktreeProcessor
|
||||
|
||||
-- Bastian Venthur <venthur@debian.org> Wed, 29 Jun 2022 21:27:15 +0200
|
||||
|
||||
blag (1.3.1) unstable; urgency=medium
|
||||
|
||||
* re-upload with man pages
|
||||
|
||||
12
debian/control
vendored
12
debian/control
vendored
@@ -5,9 +5,9 @@ Maintainer: Bastian Venthur <venthur@debian.org>
|
||||
Rules-Requires-Root: no
|
||||
Build-Depends:
|
||||
debhelper-compat (= 13),
|
||||
dh-sequence-sphinxdoc,
|
||||
dh-sequence-python3,
|
||||
dh-python,
|
||||
pybuild-plugin-pyproject,
|
||||
python3-setuptools,
|
||||
python3-all,
|
||||
python3-markdown,
|
||||
@@ -16,7 +16,9 @@ Build-Depends:
|
||||
python3-pygments,
|
||||
python3-pytest,
|
||||
python3-pytest-cov,
|
||||
python3-sphinx,
|
||||
mkdocs,
|
||||
mkdocs-material,
|
||||
mkdocstrings-python-handlers,
|
||||
#Testsuite: autopkgtest-pkg-python
|
||||
Standards-Version: 4.6.0.1
|
||||
Homepage: https://github.com/venthur/blag
|
||||
@@ -29,11 +31,12 @@ Depends:
|
||||
${python3:Depends},
|
||||
${misc:Depends},
|
||||
Suggests:
|
||||
python-blag-doc,
|
||||
blag-doc,
|
||||
Description: Blog-aware, static site generator
|
||||
Blag is a blog-aware, static site generator, written in Python. It supports
|
||||
the following 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
|
||||
@@ -44,12 +47,13 @@ Package: blag-doc
|
||||
Section: doc
|
||||
Architecture: all
|
||||
Depends:
|
||||
${sphinxdoc:Depends},
|
||||
${misc:Depends},
|
||||
Multi-Arch: foreign
|
||||
Description: Blog-aware, static site generator (documentation)
|
||||
Blag is a blog-aware, static site generator, written in Python. It supports
|
||||
the following 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
|
||||
|
||||
12
debian/rules
vendored
12
debian/rules
vendored
@@ -9,17 +9,9 @@ export PYBUILD_TEST_ARGS=--no-cov
|
||||
export PYBUILD_NAME=blag
|
||||
|
||||
%:
|
||||
dh $@ --with python3,sphinxdoc --buildsystem=pybuild
|
||||
dh $@ --with python3 --buildsystem=pybuild
|
||||
|
||||
|
||||
# If you need to rebuild the Sphinx documentation:
|
||||
# Add sphinxdoc to the dh --with line.
|
||||
#
|
||||
# And uncomment the following lines.
|
||||
execute_after_dh_auto_build-indep: export http_proxy=127.0.0.1:9
|
||||
execute_after_dh_auto_build-indep: export https_proxy=127.0.0.1:9
|
||||
execute_after_dh_auto_build-indep:
|
||||
PYTHONPATH=. python3 -m sphinx -N -bhtml \
|
||||
docs/ build/html # HTML generator
|
||||
PYTHONPATH=. python3 -m sphinx -N -bman \
|
||||
docs/ build/man # Manpage generator
|
||||
PYTHONPATH=. mkdocs build
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
11
docs/api.rst
11
docs/api.rst
@@ -1,11 +0,0 @@
|
||||
API
|
||||
===
|
||||
|
||||
.. autosummary::
|
||||
:toctree: api
|
||||
|
||||
blag.__init__
|
||||
blag.version
|
||||
blag.blag
|
||||
blag.markdown
|
||||
blag.devserver
|
||||
1
docs/blag.md
Normal file
1
docs/blag.md
Normal file
@@ -0,0 +1 @@
|
||||
::: blag.blag
|
||||
BIN
docs/blag.png
Normal file
BIN
docs/blag.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
283
docs/blag.rst
283
docs/blag.rst
@@ -1,283 +0,0 @@
|
||||
Manual
|
||||
======
|
||||
|
||||
|
||||
Quickstart
|
||||
----------
|
||||
|
||||
Install blag from PyPI_
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
$ pip install blag
|
||||
|
||||
.. _pypi: https://pypi.org/project/blag/
|
||||
|
||||
Run blag's quickstart command to create the configuration needed
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
$ blag quickstart
|
||||
|
||||
Create some content
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
$ mkdir content
|
||||
$ edit content/hello-world.md
|
||||
|
||||
Generate the website
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
$ blag build
|
||||
|
||||
By default, blag will search for content in ``content`` and the output will be
|
||||
generated in ``build``. All markdown files in ``content`` will be converted to
|
||||
html, all other files (i.e. static files) will be copied over).
|
||||
|
||||
If you want more separation between the static files and the markdown content,
|
||||
you can put all static files into the ``static`` directory. Blag will copy
|
||||
them over to the ``build`` directory.
|
||||
|
||||
If you want to customize the looks of the generated site, create a
|
||||
``template`` directory and put your jinja2 templates here.
|
||||
|
||||
Those directories can be changed via command line arguments. See
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
$ blag --help
|
||||
|
||||
|
||||
Manual
|
||||
------
|
||||
|
||||
|
||||
Pages and Articles
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
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
|
||||
|
||||
blag does **not** enforce a certain directory structure for pages and
|
||||
articles. You can mix and match them freely or structure them in different
|
||||
directories. blag will mirror the structure found in the ``content`` directory
|
||||
|
||||
::
|
||||
|
||||
content/
|
||||
article1.md
|
||||
article2.md
|
||||
page1.md
|
||||
|
||||
results in:
|
||||
|
||||
::
|
||||
|
||||
build/
|
||||
article1.html
|
||||
article2.html
|
||||
page1.html
|
||||
|
||||
Arbitrary complex structures are possible too:
|
||||
|
||||
::
|
||||
|
||||
content/
|
||||
posts/
|
||||
2020/
|
||||
2020-01-01-foo.md
|
||||
2020-02-01-foo.md
|
||||
pages/
|
||||
foo.md
|
||||
bar.md
|
||||
|
||||
results in:
|
||||
|
||||
::
|
||||
|
||||
build/
|
||||
posts/
|
||||
2020/
|
||||
2020-01-01-foo.html
|
||||
2020-02-01-foo.html
|
||||
pages/
|
||||
foo.html
|
||||
bar.html
|
||||
|
||||
|
||||
Static Files
|
||||
^^^^^^^^^^^^
|
||||
|
||||
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
|
||||
content and static files, you can create a ``static`` directory and put the
|
||||
files there. All files and directories found in the ``static`` directory will
|
||||
be copied over to ``build``.
|
||||
|
||||
::
|
||||
|
||||
content/
|
||||
foo.md
|
||||
bar.md
|
||||
kitty.jpg
|
||||
|
||||
results in:
|
||||
|
||||
::
|
||||
|
||||
build/
|
||||
foo.html
|
||||
bar.html
|
||||
kitty.jpg
|
||||
|
||||
Alternatively:
|
||||
|
||||
::
|
||||
|
||||
content/
|
||||
foo.md
|
||||
bar.md
|
||||
static/
|
||||
kitty.jpg
|
||||
|
||||
results in:
|
||||
|
||||
::
|
||||
|
||||
build/
|
||||
foo.html
|
||||
bar.html
|
||||
kitty.jpg
|
||||
|
||||
|
||||
Internal Links
|
||||
--------------
|
||||
|
||||
In contrast to most other static blog generators, blag will automatically
|
||||
convert **relative** markdown links. That means you can link you content using
|
||||
relative markdown links and blag will convert them to html automatically. The
|
||||
advantage is that your content tree in markdown is consistent and
|
||||
self-contained even if you don't generate html from it.
|
||||
|
||||
|
||||
.. code-block:: markdown
|
||||
|
||||
[...]
|
||||
this is a [link](foo.md) to an internal page foo.
|
||||
|
||||
becomes
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<p>this is a <a href="foo.html">link</a> to an internal page foo.</p>
|
||||
|
||||
|
||||
Templating
|
||||
----------
|
||||
|
||||
Custom templates are **optional** and stored by default in the ``templates``
|
||||
directory. blag will search the ``templates`` directory first, and fall back
|
||||
to blag's default built-in templates.
|
||||
|
||||
============ ====================================== ===================
|
||||
Template Used For Variables
|
||||
============ ====================================== ===================
|
||||
page.html pages (i.e. non-articles) site, content, meta
|
||||
article.html articles (i.e. blog posts) site, content, meta
|
||||
archive.html archive- and landing page of the blog site, archive
|
||||
tags.html list of tags site, tags
|
||||
tag.html archive of Articles with a certain tag site, archive, tag
|
||||
============ ====================================== ===================
|
||||
|
||||
If you make use of Jinja2's template inheritance, you can of course have more
|
||||
template files in the ``templates`` directory.
|
||||
|
||||
``site``
|
||||
This dictionary contains the site configuration, namely: ``base_url``,
|
||||
``title``, ``description`` and ``author``. Don't confuse the site-title
|
||||
and -description with the title and description of individual pages or
|
||||
articles.
|
||||
|
||||
``content``
|
||||
HTML, converted from markdown.
|
||||
|
||||
``meta``
|
||||
``meta`` stands for all metadata elements available in the article or
|
||||
page. Please be aware that those are not wrapped in a dictionary, but
|
||||
**directly** available as variables.
|
||||
|
||||
``archive``
|
||||
A list of ``[destination path, context]`` tuples, where the context are
|
||||
the respective variables that would be provided to the individual page or
|
||||
article.
|
||||
|
||||
``tags``
|
||||
List of tags.
|
||||
|
||||
``tag``
|
||||
A tag.
|
||||
|
||||
|
||||
Metadata
|
||||
---------
|
||||
|
||||
blag supports metadata elements in the markdown files. They must come before
|
||||
the content and should be separated from the content with a blank line:
|
||||
|
||||
.. code-block:: markdown
|
||||
|
||||
title: foo
|
||||
date: 2020-02-02
|
||||
tags: this, is, a, test
|
||||
description: some subtitle
|
||||
|
||||
this is my content.
|
||||
[...]
|
||||
|
||||
blag supports *arbitrary* metadata in your documents, and you can use them
|
||||
freely in you templates. However, some metadata elements are treated special:
|
||||
|
||||
``date``
|
||||
If a document contains the ``date`` element, it is treated as an
|
||||
**article**, otherwise as a **page**. Additionally, ``date`` elements are
|
||||
expected to be in ISO format (e.g. ``1980-05-05 21:58``). They are
|
||||
automatically converted into ``datetime`` objects with the local timezone
|
||||
attached.
|
||||
|
||||
``tags``
|
||||
Tags are interpreted as a comma separated list. All elements are stripped
|
||||
and converted to lower-case: ``tags: foo, Foo Bar, BAZ`` becomes: ``[foo,
|
||||
foo bar, baz]``.
|
||||
|
||||
Tags in **articles** are also used to generate the tag-pages, that
|
||||
aggregate all articles per tag.
|
||||
|
||||
``title`` and ``description``
|
||||
The title and description are used in the html header and in the atom
|
||||
feed.
|
||||
|
||||
|
||||
Devserver
|
||||
---------
|
||||
|
||||
blag provides a devserver which you can use for local web-development. The
|
||||
devserver provides a simple web server, serving your site in
|
||||
http://localhost:8000 and will automatically rebuild the project when it
|
||||
detects modifications in one of the ``content``, ``static`` and ``templates``
|
||||
directories.
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
$ blag serve
|
||||
|
||||
1
docs/blag_.md
Normal file
1
docs/blag_.md
Normal file
@@ -0,0 +1 @@
|
||||
::: blag
|
||||
69
docs/conf.py
69
docs/conf.py
@@ -1,69 +0,0 @@
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# This file only contains a selection of the most common options. For a full
|
||||
# list see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
|
||||
import blag
|
||||
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'blag'
|
||||
copyright = '2021, Bastian Venthur'
|
||||
author = 'Bastian Venthur'
|
||||
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = blag.__VERSION__
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autosummary',
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.napoleon',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = 'alabaster'
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
autodoc_default_options = {
|
||||
'members': True,
|
||||
'undoc-members': True,
|
||||
'private-members': True,
|
||||
'special-members': True,
|
||||
}
|
||||
|
||||
autosummary_generate = True
|
||||
1
docs/devserver.md
Normal file
1
docs/devserver.md
Normal file
@@ -0,0 +1 @@
|
||||
::: blag.devserver
|
||||
57
docs/index.md
Normal file
57
docs/index.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Welcome to blag!
|
||||
|
||||
blag is a blog-aware, static site generator, written in [Python][].
|
||||
|
||||
* an example "deployment" can be found [here][venthur.de]
|
||||
* online [documentation][] is available on https://readthedocs.org.
|
||||
|
||||
blag is named after [the blag of the webcomic xkcd][blagxkcd].
|
||||
|
||||
[python]: https://python.org
|
||||
[blagxkcd]: https://blog.xkcd.com
|
||||
[venthur.de]: https://venthur.de
|
||||
[documentation]: https://blag.readthedocs.io/en/latest/
|
||||
|
||||
|
||||
## 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][]
|
||||
* Integrated devserver
|
||||
* Available on [PyPI][]
|
||||
|
||||
blag runs on Linux, Mac and Windows and requires Python >= 3.8
|
||||
|
||||
[markdown]: https://daringfireball.net/projects/markdown/
|
||||
[jinja2]: https://palletsprojects.com/p/jinja/
|
||||
[pygments]: https://pygments.org/
|
||||
[pypi]: https://pypi.org/project/blag/
|
||||
|
||||
|
||||
## Install
|
||||
|
||||
blag is available on [PyPI][], you can install it via:
|
||||
|
||||
```bash
|
||||
$ pip install blag
|
||||
```
|
||||
|
||||
On Debian or Ubuntu, you can also just install the Debian package:
|
||||
|
||||
```bash
|
||||
$ sudo aptitude install blag
|
||||
```
|
||||
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
$ pip install blag # 1. install blag
|
||||
$ blag quickstart # 2. create a new site
|
||||
$ vim content/hello-world.md # 3. create some content
|
||||
$ blag build # 4. build the website
|
||||
```
|
||||
@@ -1,53 +0,0 @@
|
||||
.. blag documentation master file, created by
|
||||
sphinx-quickstart on Sun Mar 21 13:39:00 2021.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to blag!
|
||||
================
|
||||
|
||||
blag is a blog-aware, static site generator, written in Python_. An example
|
||||
"deployment" can be found here_.
|
||||
|
||||
blag is named after the blag of the webcomic xkcd_.
|
||||
|
||||
.. _python: https://python.org
|
||||
.. _xkcd: https://blog.xkcd.com
|
||||
.. _here: https://venthur.de
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
* Write content in Markdown_
|
||||
* Theming support using Jinja2_ templates
|
||||
* Generation of Atom feeds for blog content
|
||||
* Fenced code blocks and syntax highlighting using Pygments_
|
||||
* Integrated devserver
|
||||
* Available on PyPI_
|
||||
|
||||
blag runs on Linux, Mac and Windows and requires Python >= 3.8
|
||||
|
||||
.. _markdown: https://daringfireball.net/projects/markdown/
|
||||
.. _jinja2: https://palletsprojects.com/p/jinja/
|
||||
.. _pygments: https://pygments.org/
|
||||
.. _pypi: https://pypi.org/project/blag/
|
||||
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
|
||||
blag.rst
|
||||
api.rst
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
@@ -1,35 +0,0 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
|
||||
:end
|
||||
popd
|
||||
264
docs/manual.md
Normal file
264
docs/manual.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# Manual
|
||||
|
||||
|
||||
## Quickstart
|
||||
|
||||
Install blag from [PyPI][]
|
||||
|
||||
```sh
|
||||
$ pip install blag
|
||||
```
|
||||
|
||||
[pypi]: https://pypi.org/project/blag/
|
||||
|
||||
Run blag's quickstart command to create the configuration, templates and some
|
||||
initial content.
|
||||
|
||||
```sh
|
||||
$ blag quickstart
|
||||
```
|
||||
|
||||
Create some content
|
||||
|
||||
```sh
|
||||
$ edit content/hello-world.md
|
||||
```
|
||||
|
||||
Generate the website
|
||||
|
||||
```sh
|
||||
$ blag build
|
||||
```
|
||||
|
||||
By default, blag will search for content in `content` and the output will be
|
||||
generated in `build`. All markdown files in `content` will be converted to
|
||||
html, all other files (i.e. static files) will be copied over).
|
||||
|
||||
If you want more separation between the static files and the markdown content,
|
||||
you can put all static files into the `static` directory. Blag will copy them
|
||||
over to the `build` directory.
|
||||
|
||||
If you want to customize the look of the generated site, visit the `template`
|
||||
directory. It contains jinja2 templates and can be modified as needed.
|
||||
|
||||
Those directories can be changed via command line arguments. See
|
||||
|
||||
```sh
|
||||
$ blag --help
|
||||
```
|
||||
|
||||
|
||||
## Manual
|
||||
|
||||
### Pages and Articles
|
||||
|
||||
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
|
||||
|
||||
blag does **not** enforce a certain directory structure for pages and articles.
|
||||
You can mix and match them freely or structure them in different directories.
|
||||
blag will mirror the structure found in the `content` directory
|
||||
|
||||
```
|
||||
content/
|
||||
article1.md
|
||||
article2.md
|
||||
page1.md
|
||||
```
|
||||
|
||||
results in:
|
||||
|
||||
```
|
||||
build/
|
||||
article1.html
|
||||
article2.html
|
||||
page1.html
|
||||
```
|
||||
|
||||
Arbitrary complex structures are possible too:
|
||||
|
||||
```
|
||||
content/
|
||||
posts/
|
||||
2020/
|
||||
2020-01-01-foo.md
|
||||
2020-02-01-foo.md
|
||||
pages/
|
||||
foo.md
|
||||
bar.md
|
||||
```
|
||||
|
||||
results in:
|
||||
|
||||
```
|
||||
build/
|
||||
posts/
|
||||
2020/
|
||||
2020-01-01-foo.html
|
||||
2020-02-01-foo.html
|
||||
pages/
|
||||
foo.html
|
||||
bar.html
|
||||
```
|
||||
|
||||
|
||||
### Static Files
|
||||
|
||||
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 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 be copied over
|
||||
to `build`.
|
||||
|
||||
```
|
||||
content/
|
||||
foo.md
|
||||
bar.md
|
||||
kitty.jpg
|
||||
```
|
||||
|
||||
results in:
|
||||
|
||||
```
|
||||
build/
|
||||
foo.html
|
||||
bar.html
|
||||
kitty.jpg
|
||||
```
|
||||
|
||||
Alternatively:
|
||||
|
||||
```
|
||||
content/
|
||||
foo.md
|
||||
bar.md
|
||||
static/
|
||||
kitty.jpg
|
||||
```
|
||||
|
||||
results in:
|
||||
|
||||
```
|
||||
build/
|
||||
foo.html
|
||||
bar.html
|
||||
kitty.jpg
|
||||
```
|
||||
|
||||
|
||||
### Internal Links
|
||||
|
||||
In contrast to most other static blog generators, blag will automatically
|
||||
convert **relative** markdown links. That means you can link you content using
|
||||
relative markdown links and blag will convert them to html automatically. The
|
||||
advantage is that your content tree in markdown is consistent and
|
||||
self-contained even if you don't generate html from it.
|
||||
|
||||
|
||||
```markdown
|
||||
[...]
|
||||
this is a [link](foo.md) to an internal page foo.
|
||||
```
|
||||
|
||||
becomes
|
||||
|
||||
```html
|
||||
<p>this is a <a href="foo.html">link</a> to an internal page foo.</p>
|
||||
```
|
||||
|
||||
```python
|
||||
def this_is_a(test):
|
||||
pass
|
||||
```
|
||||
|
||||
### Templating
|
||||
|
||||
Templates are stored by default in the `templates` directory.
|
||||
|
||||
Template | Used For | Variables
|
||||
------------ | -------------------------------------- | -------------------
|
||||
page.html | pages (i.e. non-articles) | site, content, meta
|
||||
article.html | articles (i.e. blog posts) | site, content, meta
|
||||
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
|
||||
tag.html | archive of Articles with a certain tag | site, archive, tag
|
||||
|
||||
If you make use of Jinja2's template inheritance, you can of course have more
|
||||
template files in the `templates` directory.
|
||||
|
||||
|
||||
#### Variables
|
||||
|
||||
* `site`: This dictionary contains the site configuration, namely: `base_url`,
|
||||
`title`, `description` and `author`. Don't confuse the site-title and
|
||||
-description with the title and description of individual pages or articles.
|
||||
|
||||
* `content`: HTML, converted from markdown.
|
||||
|
||||
* `meta`: stands for all metadata elements available in the article or page.
|
||||
Please be aware that those are not wrapped in a dictionary, but **directly**
|
||||
available as variables.
|
||||
|
||||
* `archive`: A list of `[destination path, context]` tuples, where the context
|
||||
are the respective variables that would be provided to the individual page or
|
||||
article.
|
||||
|
||||
* `tags`: List of tags.
|
||||
|
||||
* `tag`: A tag.
|
||||
|
||||
|
||||
### Metadata
|
||||
|
||||
blag supports metadata elements in the markdown files. They must come before
|
||||
the content and should be separated from the content with a blank line:
|
||||
|
||||
```markdown
|
||||
title: foo
|
||||
date: 2020-02-02
|
||||
tags: this, is, a, test
|
||||
description: some subtitle
|
||||
|
||||
this is my content.
|
||||
[...]
|
||||
```
|
||||
|
||||
blag supports *arbitrary* metadata in your documents, and you can use them
|
||||
freely in you templates. However, some metadata elements are treated special:
|
||||
|
||||
* `date`: If a document contains the `date` element, it is treated as an
|
||||
**article**, otherwise as a **page**. Additionally, `date` elements are
|
||||
expected to be in ISO format (e.g. `1980-05-09 21:58`). They are
|
||||
automatically converted into `datetime` objects with the local timezone
|
||||
attached.
|
||||
|
||||
* `tags`: Tags are interpreted as a comma separated list. All elements are
|
||||
stripped and converted to lower-case: `tags: foo, Foo Bar, BAZ` becomes:
|
||||
`[foo, foo bar, baz]`. Tags in **articles** are also used to generate the
|
||||
tag-pages, that aggregate all articles per tag.
|
||||
|
||||
* `title` and `description`: The title and description are used in the html
|
||||
header and in the atom feed.
|
||||
|
||||
|
||||
## Devserver
|
||||
|
||||
blag provides a devserver which you can use for local web-development. The
|
||||
devserver provides a simple web server, serving your site in
|
||||
http://localhost:8000 and will automatically rebuild the project when it
|
||||
detects modifications in one of the `content`, `static` and `templates`
|
||||
directories.
|
||||
|
||||
```sh
|
||||
$ blag serve
|
||||
```
|
||||
1
docs/markdown.md
Normal file
1
docs/markdown.md
Normal file
@@ -0,0 +1 @@
|
||||
::: blag.markdown
|
||||
1
docs/quickstart.md
Normal file
1
docs/quickstart.md
Normal file
@@ -0,0 +1 @@
|
||||
::: blag.quickstart
|
||||
1
docs/version.md
Normal file
1
docs/version.md
Normal file
@@ -0,0 +1 @@
|
||||
::: blag.version
|
||||
30
mkdocs.yml
Normal file
30
mkdocs.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
site_name: blag
|
||||
site_url: https://blag.readthedocs.io/
|
||||
repo_url: https://github.com/venthur/blag
|
||||
repo_name: venthur/blag
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Manual: manual.md
|
||||
- API:
|
||||
- blag: blag_.md
|
||||
- blag.version: version.md
|
||||
- blag.blag: blag.md
|
||||
- blag.markdown: markdown.md
|
||||
- blag.devserver: devserver.md
|
||||
- blag.quickstart: quickstart.md
|
||||
|
||||
theme:
|
||||
name: material
|
||||
highlightjs: true
|
||||
|
||||
markdown_extensions:
|
||||
- pymdownx.superfences
|
||||
|
||||
plugins:
|
||||
- search:
|
||||
- mkdocstrings:
|
||||
handlers:
|
||||
python:
|
||||
options:
|
||||
docstring_style: numpy
|
||||
87
pyproject.toml
Normal file
87
pyproject.toml
Normal file
@@ -0,0 +1,87 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=64.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "blag"
|
||||
authors = [
|
||||
{ name="Bastian Venthur", email="mail@venthur.de" },
|
||||
]
|
||||
description = "blog-aware, static site generator"
|
||||
keywords = ["markdown", "blag", "blog", "static site generator", "cli"]
|
||||
readme = "README.md"
|
||||
license = { file="LICENSE" }
|
||||
requires-python = ">=3.8"
|
||||
dynamic = ["version"]
|
||||
dependencies = [
|
||||
"markdown",
|
||||
"feedgenerator",
|
||||
"jinja2",
|
||||
"pygments",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
blag = "blag.blag:main"
|
||||
|
||||
[project.urls]
|
||||
'Documentation' = 'https://blag.readthedocs.io/'
|
||||
'Source' = 'https://github.com/venthur/blag'
|
||||
'Changelog' = 'https://github.com/venthur/blag/blob/master/CHANGELOG.md'
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"build",
|
||||
"mkdocs",
|
||||
"mkdocs-material",
|
||||
"mkdocstrings[python]",
|
||||
"twine",
|
||||
"wheel",
|
||||
"pytest",
|
||||
"pytest-cov",
|
||||
"ruff",
|
||||
"mypy",
|
||||
"types-markdown",
|
||||
]
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "blag.__VERSION__" }
|
||||
|
||||
[tool.setuptools]
|
||||
packages = [
|
||||
"blag",
|
||||
"blag.templates",
|
||||
"blag.static",
|
||||
"blag.content",
|
||||
"tests",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = """
|
||||
--cov=blag
|
||||
--cov=tests
|
||||
--cov-report=html
|
||||
--cov-report=term-missing:skip-covered
|
||||
"""
|
||||
|
||||
[tool.ruff]
|
||||
select = [
|
||||
"F", # pyflakes
|
||||
"E", "W", # pycodestyle
|
||||
"C90", # mccabe
|
||||
"I", # isort
|
||||
"D", # pydocstyle
|
||||
"UP" # pyupgrade
|
||||
]
|
||||
line-length = 79
|
||||
target-version = "py38"
|
||||
|
||||
[tool.ruff.pydocstyle]
|
||||
convention = "numpy"
|
||||
|
||||
[tool.mypy]
|
||||
files = "blag,tests"
|
||||
strict = true
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "feedgenerator.*"
|
||||
ignore_missing_imports = true
|
||||
@@ -1,6 +1,11 @@
|
||||
sphinx==5.0.0
|
||||
twine==4.0.0
|
||||
wheel==0.37.1
|
||||
pytest==7.1.2
|
||||
pytest-cov==3.0.0
|
||||
flake8==4.0.1
|
||||
build==1.0.3
|
||||
mkdocs==1.5.3
|
||||
mkdocs-material==9.4.8
|
||||
mkdocstrings[python]==0.23.0
|
||||
twine==4.0.2
|
||||
wheel==0.41.3
|
||||
pytest==7.4.3
|
||||
pytest-cov==4.1.0
|
||||
ruff==0.1.5
|
||||
mypy==1.6.1
|
||||
types-markdown==3.5.0.1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
markdown==3.3.7
|
||||
feedgenerator==2.0.0
|
||||
markdown==3.5.1
|
||||
feedgenerator==2.1.0
|
||||
jinja2==3.1.2
|
||||
pygments==2.12.0
|
||||
pygments==2.16.1
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
[tool:pytest]
|
||||
addopts =
|
||||
--cov=blag
|
||||
--cov=tests
|
||||
--cov-report=html
|
||||
--cov-report=term-missing:skip-covered
|
||||
|
||||
[flake8]
|
||||
exclude = venv,build,docs
|
||||
42
setup.py
42
setup.py
@@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
meta = {}
|
||||
exec(open('./blag/version.py').read(), meta)
|
||||
meta['long_description'] = open('./README.md').read()
|
||||
|
||||
setup(
|
||||
name='blag',
|
||||
version=meta['__VERSION__'],
|
||||
description='blog-aware, static site generator',
|
||||
long_description=meta['long_description'],
|
||||
long_description_content_type='text/markdown',
|
||||
keywords='markdown blag blog static site generator cli',
|
||||
author='Bastian Venthur',
|
||||
author_email='mail@venthur.de',
|
||||
url='https://github.com/venthur/blag',
|
||||
project_urls={
|
||||
'Documentation': 'https://blag.readthedocs.io/',
|
||||
'Source': 'https://github.com/venthur/blag',
|
||||
'Changelog':
|
||||
'https://github.com/venthur/blag/blob/master/CHANGELOG.md',
|
||||
},
|
||||
python_requires='>=3.8',
|
||||
package_data={
|
||||
'blag': ['templates/*'],
|
||||
},
|
||||
install_requires=[
|
||||
'markdown',
|
||||
'feedgenerator',
|
||||
'jinja2',
|
||||
'pygments',
|
||||
],
|
||||
packages=['blag'],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'blag = blag.blag:main'
|
||||
]
|
||||
},
|
||||
license='MIT',
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
"""Tests for blag."""
|
||||
|
||||
@@ -1,53 +1,72 @@
|
||||
from tempfile import TemporaryDirectory
|
||||
"""Pytest fixtures."""
|
||||
|
||||
|
||||
# remove when we don't support py38 anymore
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from argparse import Namespace
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Callable, Iterator
|
||||
|
||||
import pytest
|
||||
from jinja2 import Environment, Template
|
||||
|
||||
from blag import blag
|
||||
from blag import blag, quickstart
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def environment():
|
||||
def environment(cleandir: str) -> Iterator[Environment]:
|
||||
"""Create a Jinja2 environment."""
|
||||
site = {
|
||||
'base_url': 'site base_url',
|
||||
'title': 'site title',
|
||||
'description': 'site description',
|
||||
'author': 'site author',
|
||||
"base_url": "site base_url",
|
||||
"title": "site title",
|
||||
"description": "site description",
|
||||
"author": "site author",
|
||||
}
|
||||
env = blag.environment_factory(globals_=dict(site=site))
|
||||
env = blag.environment_factory("templates", globals_=dict(site=site))
|
||||
yield env
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def page_template(environment):
|
||||
yield environment.get_template('page.html')
|
||||
def page_template(environment: Environment) -> Iterator[Template]:
|
||||
"""Create a Jinja2 page-template."""
|
||||
yield environment.get_template("page.html")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def article_template(environment):
|
||||
yield environment.get_template('article.html')
|
||||
def article_template(environment: Environment) -> Iterator[Template]:
|
||||
"""Create a Jinja2 article-template."""
|
||||
yield environment.get_template("article.html")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def archive_template(environment):
|
||||
yield environment.get_template('archive.html')
|
||||
def index_template(environment: Environment) -> Iterator[Template]:
|
||||
"""Create a Jinja2 index-template."""
|
||||
yield environment.get_template("index.html")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tags_template(environment):
|
||||
yield environment.get_template('tags.html')
|
||||
def archive_template(environment: Environment) -> Iterator[Template]:
|
||||
"""Create a Jinja2 archive-template."""
|
||||
yield environment.get_template("archive.html")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tag_template(environment):
|
||||
yield environment.get_template('tag.html')
|
||||
def tags_template(environment: Environment) -> Iterator[Template]:
|
||||
"""Create a Jinja2 tags-template."""
|
||||
yield environment.get_template("tags.html")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cleandir():
|
||||
"""Create a temporary workind directory and cwd.
|
||||
def tag_template(environment: Environment) -> Iterator[Template]:
|
||||
"""Create a Jinja2 tag-template."""
|
||||
yield environment.get_template("tag.html")
|
||||
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def cleandir() -> Iterator[str]:
|
||||
"""Create a temporary working directory and cwd."""
|
||||
config = """
|
||||
[main]
|
||||
base_url = https://example.com/
|
||||
@@ -57,30 +76,25 @@ author = a. u. thor
|
||||
"""
|
||||
|
||||
with TemporaryDirectory() as dir:
|
||||
for d in 'content', 'build', 'static', 'templates':
|
||||
os.mkdir(f'{dir}/{d}')
|
||||
with open(f'{dir}/config.ini', 'w') as fh:
|
||||
os.mkdir(f"{dir}/build")
|
||||
with open(f"{dir}/config.ini", "w") as fh:
|
||||
fh.write(config)
|
||||
# change directory
|
||||
old_cwd = os.getcwd()
|
||||
os.chdir(dir)
|
||||
quickstart.copy_default_theme()
|
||||
yield dir
|
||||
# and change back afterwards
|
||||
os.chdir(old_cwd)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def args(cleandir):
|
||||
|
||||
class NameSpace:
|
||||
def __init__(self, **kwargs):
|
||||
for name in kwargs:
|
||||
setattr(self, name, kwargs[name])
|
||||
|
||||
args = NameSpace(
|
||||
input_dir='content',
|
||||
output_dir='build',
|
||||
static_dir='static',
|
||||
template_dir='templates',
|
||||
def args(cleandir: Callable[[], Iterator[str]]) -> Iterator[Namespace]:
|
||||
"""Create a Namespace with default arguments."""
|
||||
args = Namespace(
|
||||
input_dir="content",
|
||||
output_dir="build",
|
||||
static_dir="static",
|
||||
template_dir="templates",
|
||||
)
|
||||
yield args
|
||||
|
||||
@@ -1,122 +1,144 @@
|
||||
from tempfile import TemporaryDirectory
|
||||
"""Test blag."""
|
||||
|
||||
|
||||
# remove when we don't support py38 anymore
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from argparse import Namespace
|
||||
from datetime import datetime
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from jinja2 import Template
|
||||
from pytest import CaptureFixture, LogCaptureFixture
|
||||
|
||||
from blag import blag
|
||||
from blag import __VERSION__, blag
|
||||
|
||||
|
||||
def test_generate_feed(cleandir):
|
||||
articles = []
|
||||
blag.generate_feed(articles, 'build', ' ', ' ', ' ', ' ')
|
||||
assert os.path.exists('build/atom.xml')
|
||||
def test_generate_feed(cleandir: str) -> None:
|
||||
"""Test generate_feed."""
|
||||
articles: list[tuple[str, dict[str, Any]]] = []
|
||||
blag.generate_feed(articles, "build", " ", " ", " ", " ")
|
||||
assert os.path.exists("build/atom.xml")
|
||||
|
||||
|
||||
def test_feed(cleandir):
|
||||
articles = [
|
||||
[
|
||||
'dest1.html',
|
||||
def test_feed(cleandir: str) -> None:
|
||||
"""Test feed."""
|
||||
articles: list[tuple[str, dict[str, Any]]] = [
|
||||
(
|
||||
"dest1.html",
|
||||
{
|
||||
'title': 'title1',
|
||||
'date': datetime(2019, 6, 6),
|
||||
'content': 'content1',
|
||||
}
|
||||
],
|
||||
[
|
||||
'dest2.html',
|
||||
"title": "title1",
|
||||
"date": datetime(2019, 6, 6),
|
||||
"content": "content1",
|
||||
},
|
||||
),
|
||||
(
|
||||
"dest2.html",
|
||||
{
|
||||
'title': 'title2',
|
||||
'date': datetime(1980, 5, 9),
|
||||
'content': 'content2',
|
||||
}
|
||||
],
|
||||
|
||||
"title": "title2",
|
||||
"date": datetime(1980, 5, 9),
|
||||
"content": "content2",
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
blag.generate_feed(articles, 'build', 'https://example.com/',
|
||||
'blog title', 'blog description', 'blog author')
|
||||
with open('build/atom.xml') as fh:
|
||||
blag.generate_feed(
|
||||
articles,
|
||||
"build",
|
||||
"https://example.com/",
|
||||
"blog title",
|
||||
"blog description",
|
||||
"blog author",
|
||||
)
|
||||
with open("build/atom.xml") as fh:
|
||||
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
|
||||
# is fixed
|
||||
# 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
|
||||
assert '<title>title1</title>' in feed
|
||||
assert "<title>title1</title>" 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 '<link href="https://example.com/dest1.html"' in feed
|
||||
|
||||
# article 2
|
||||
assert '<title>title2</title>' in feed
|
||||
assert "<title>title2</title>" 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 '<link href="https://example.com/dest2.html"' in feed
|
||||
|
||||
|
||||
def test_generate_feed_with_description(cleandir):
|
||||
def test_generate_feed_with_description(cleandir: str) -> None:
|
||||
"""Test generate_feed with description."""
|
||||
# if a description is provided, it will be used as the summary in
|
||||
# the feed, otherwise we simply use the title of the article
|
||||
articles = [[
|
||||
'dest.html',
|
||||
{
|
||||
'title': 'title',
|
||||
'description': 'description',
|
||||
'date': datetime(2019, 6, 6),
|
||||
'content': 'content',
|
||||
}
|
||||
]]
|
||||
blag.generate_feed(articles, 'build', ' ', ' ', ' ', ' ')
|
||||
articles: list[tuple[str, dict[str, Any]]] = [
|
||||
(
|
||||
"dest.html",
|
||||
{
|
||||
"title": "title",
|
||||
"description": "description",
|
||||
"date": datetime(2019, 6, 6),
|
||||
"content": "content",
|
||||
},
|
||||
)
|
||||
]
|
||||
blag.generate_feed(articles, "build", " ", " ", " ", " ")
|
||||
|
||||
with open('build/atom.xml') as fh:
|
||||
with open("build/atom.xml") as fh:
|
||||
feed = fh.read()
|
||||
|
||||
assert '<title>title</title>' in feed
|
||||
assert "<title>title</title>" 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
|
||||
|
||||
|
||||
def test_parse_args_build():
|
||||
def test_parse_args_build() -> None:
|
||||
"""Test parse_args with build."""
|
||||
# test default args
|
||||
args = blag.parse_args(['build'])
|
||||
assert args.input_dir == 'content'
|
||||
assert args.output_dir == 'build'
|
||||
assert args.template_dir == 'templates'
|
||||
assert args.static_dir == 'static'
|
||||
args = blag.parse_args(["build"])
|
||||
assert args.input_dir == "content"
|
||||
assert args.output_dir == "build"
|
||||
assert args.template_dir == "templates"
|
||||
assert args.static_dir == "static"
|
||||
|
||||
# input dir
|
||||
args = blag.parse_args(['build', '-i', 'foo'])
|
||||
assert args.input_dir == 'foo'
|
||||
args = blag.parse_args(['build', '--input-dir', 'foo'])
|
||||
assert args.input_dir == 'foo'
|
||||
args = blag.parse_args(["build", "-i", "foo"])
|
||||
assert args.input_dir == "foo"
|
||||
args = blag.parse_args(["build", "--input-dir", "foo"])
|
||||
assert args.input_dir == "foo"
|
||||
|
||||
# output dir
|
||||
args = blag.parse_args(['build', '-o', 'foo'])
|
||||
assert args.output_dir == 'foo'
|
||||
args = blag.parse_args(['build', '--output-dir', 'foo'])
|
||||
assert args.output_dir == 'foo'
|
||||
args = blag.parse_args(["build", "-o", "foo"])
|
||||
assert args.output_dir == "foo"
|
||||
args = blag.parse_args(["build", "--output-dir", "foo"])
|
||||
assert args.output_dir == "foo"
|
||||
|
||||
# template dir
|
||||
args = blag.parse_args(['build', '-t', 'foo'])
|
||||
assert args.template_dir == 'foo'
|
||||
args = blag.parse_args(['build', '--template-dir', 'foo'])
|
||||
assert args.template_dir == 'foo'
|
||||
args = blag.parse_args(["build", "-t", "foo"])
|
||||
assert args.template_dir == "foo"
|
||||
args = blag.parse_args(["build", "--template-dir", "foo"])
|
||||
assert args.template_dir == "foo"
|
||||
|
||||
# static dir
|
||||
args = blag.parse_args(['build', '-s', 'foo'])
|
||||
assert args.static_dir == 'foo'
|
||||
args = blag.parse_args(['build', '--static-dir', 'foo'])
|
||||
assert args.static_dir == 'foo'
|
||||
args = blag.parse_args(["build", "-s", "foo"])
|
||||
assert args.static_dir == "foo"
|
||||
args = blag.parse_args(["build", "--static-dir", "foo"])
|
||||
assert args.static_dir == "foo"
|
||||
|
||||
|
||||
def test_get_config():
|
||||
def test_get_config() -> None:
|
||||
"""Test get_config."""
|
||||
config = """
|
||||
[main]
|
||||
base_url = https://example.com/
|
||||
@@ -126,25 +148,24 @@ author = a. u. thor
|
||||
"""
|
||||
# happy path
|
||||
with TemporaryDirectory() as dir:
|
||||
configfile = f'{dir}/config.ini'
|
||||
with open(configfile, 'w') as fh:
|
||||
configfile = f"{dir}/config.ini"
|
||||
with open(configfile, "w") as fh:
|
||||
fh.write(config)
|
||||
|
||||
config_parsed = blag.get_config(configfile)
|
||||
assert config_parsed['base_url'] == 'https://example.com/'
|
||||
assert config_parsed['title'] == 'title'
|
||||
assert config_parsed['description'] == 'description'
|
||||
assert config_parsed['author'] == 'a. u. thor'
|
||||
assert config_parsed["base_url"] == "https://example.com/"
|
||||
assert config_parsed["title"] == "title"
|
||||
assert config_parsed["description"] == "description"
|
||||
assert config_parsed["author"] == "a. u. thor"
|
||||
|
||||
# a missing required config causes a sys.exit
|
||||
for x in 'base_url', 'title', 'description', 'author':
|
||||
config2 = '\n'.join([line
|
||||
for line
|
||||
in config.splitlines()
|
||||
if not line.startswith(x)])
|
||||
for x in "base_url", "title", "description", "author":
|
||||
config2 = "\n".join(
|
||||
[line for line in config.splitlines() if not line.startswith(x)]
|
||||
)
|
||||
with TemporaryDirectory() as dir:
|
||||
configfile = f'{dir}/config.ini'
|
||||
with open(configfile, 'w') as fh:
|
||||
configfile = f"{dir}/config.ini"
|
||||
with open(configfile, "w") as fh:
|
||||
fh.write(config2)
|
||||
with pytest.raises(SystemExit):
|
||||
config_parsed = blag.get_config(configfile)
|
||||
@@ -158,25 +179,28 @@ description = description
|
||||
author = a. u. thor
|
||||
"""
|
||||
with TemporaryDirectory() as dir:
|
||||
configfile = f'{dir}/config.ini'
|
||||
with open(configfile, 'w') as fh:
|
||||
configfile = f"{dir}/config.ini"
|
||||
with open(configfile, "w") as fh:
|
||||
fh.write(config)
|
||||
|
||||
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():
|
||||
globals_ = {
|
||||
'foo': 'bar',
|
||||
'test': 'me'
|
||||
}
|
||||
env = blag.environment_factory(globals_=globals_)
|
||||
assert env.globals['foo'] == 'bar'
|
||||
assert env.globals['test'] == 'me'
|
||||
def test_environment_factory(cleandir: str) -> None:
|
||||
"""Test environment_factory."""
|
||||
globals_: dict[str, object] = {"foo": "bar", "test": "me"}
|
||||
env = blag.environment_factory("templates", globals_=globals_)
|
||||
assert env.globals["foo"] == "bar"
|
||||
assert env.globals["test"] == "me"
|
||||
|
||||
|
||||
def test_process_markdown(cleandir, page_template, article_template):
|
||||
def test_process_markdown(
|
||||
cleandir: str,
|
||||
page_template: Template,
|
||||
article_template: Template,
|
||||
) -> None:
|
||||
"""Test process_markdown."""
|
||||
page1 = """\
|
||||
title: some page
|
||||
|
||||
@@ -202,17 +226,12 @@ foo bar
|
||||
|
||||
convertibles = []
|
||||
for i, txt in enumerate((page1, article1, article2)):
|
||||
i = str(i)
|
||||
with open(f'content/{i}', 'w') as fh:
|
||||
with open(f"content/{str(i)}", "w") as fh:
|
||||
fh.write(txt)
|
||||
convertibles.append([i, i])
|
||||
convertibles.append((str(i), str(i)))
|
||||
|
||||
articles, pages = blag.process_markdown(
|
||||
convertibles,
|
||||
'content',
|
||||
'build',
|
||||
page_template,
|
||||
article_template
|
||||
convertibles, "content", "build", page_template, article_template
|
||||
)
|
||||
|
||||
assert isinstance(articles, list)
|
||||
@@ -220,17 +239,18 @@ foo bar
|
||||
for dst, context in articles:
|
||||
assert isinstance(dst, str)
|
||||
assert isinstance(context, dict)
|
||||
assert 'content' in context
|
||||
assert "content" in context
|
||||
|
||||
assert isinstance(pages, list)
|
||||
assert len(pages) == 1
|
||||
for dst, context in pages:
|
||||
assert isinstance(dst, str)
|
||||
assert isinstance(context, dict)
|
||||
assert 'content' in context
|
||||
assert "content" in context
|
||||
|
||||
|
||||
def test_build(args):
|
||||
def test_build(args: Namespace) -> None:
|
||||
"""Test build."""
|
||||
page1 = """\
|
||||
title: some page
|
||||
|
||||
@@ -259,21 +279,77 @@ foo bar
|
||||
# write some convertibles
|
||||
convertibles = []
|
||||
for i, txt in enumerate((page1, article1, article2)):
|
||||
i = str(i)
|
||||
with open(f'{args.input_dir}/{i}.md', 'w') as fh:
|
||||
with open(f"{args.input_dir}/{str(i)}.md", "w") as fh:
|
||||
fh.write(txt)
|
||||
convertibles.append([i, i])
|
||||
convertibles.append((str(i), str(i)))
|
||||
|
||||
# some static files
|
||||
with open(f'{args.static_dir}/test', 'w') as fh:
|
||||
fh.write('hello')
|
||||
with open(f"{args.static_dir}/test", "w") as fh:
|
||||
fh.write("hello")
|
||||
|
||||
os.mkdir(f'{args.input_dir}/testdir')
|
||||
with open(f'{args.input_dir}/testdir/test', 'w') as fh:
|
||||
fh.write('hello')
|
||||
os.mkdir(f"{args.input_dir}/testdir")
|
||||
with open(f"{args.input_dir}/testdir/test", "w") as fh:
|
||||
fh.write("hello")
|
||||
|
||||
blag.build(args)
|
||||
|
||||
# test existence of the three converted files
|
||||
for i in range(3):
|
||||
assert os.path.exists(f"{args.output_dir}/{i}.html")
|
||||
# ... static file
|
||||
assert os.path.exists(f"{args.output_dir}/test")
|
||||
# ... directory
|
||||
assert os.path.exists(f"{args.output_dir}/testdir/test")
|
||||
# ... feed
|
||||
assert os.path.exists(f"{args.output_dir}/atom.xml")
|
||||
# ... index
|
||||
assert os.path.exists(f"{args.output_dir}/index.html")
|
||||
# ... archive
|
||||
assert os.path.exists(f"{args.output_dir}/archive.html")
|
||||
# ... tags
|
||||
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/bar.html")
|
||||
|
||||
def test_main(cleandir):
|
||||
blag.main(['build'])
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"template",
|
||||
[
|
||||
"page.html",
|
||||
"article.html",
|
||||
"index.html",
|
||||
"archive.html",
|
||||
"tags.html",
|
||||
"tag.html",
|
||||
],
|
||||
)
|
||||
def test_missing_template_raises(template: str, args: Namespace) -> None:
|
||||
"""Test that missing templates raise SystemExit."""
|
||||
os.remove(f"templates/{template}")
|
||||
with pytest.raises(SystemExit):
|
||||
blag.build(args)
|
||||
|
||||
|
||||
def test_main(cleandir: str) -> None:
|
||||
"""Test main."""
|
||||
blag.main(["build"])
|
||||
|
||||
|
||||
def test_cli_version(capsys: CaptureFixture[str]) -> None:
|
||||
"""Test --version."""
|
||||
with pytest.raises(SystemExit) as ex:
|
||||
blag.main(["--version"])
|
||||
# normal system exit
|
||||
assert ex.value.code == 0
|
||||
# proper version reported
|
||||
out, _ = capsys.readouterr()
|
||||
assert __VERSION__ in out
|
||||
|
||||
|
||||
def test_cli_verbose(cleandir: str, caplog: LogCaptureFixture) -> None:
|
||||
"""Test --verbose."""
|
||||
blag.main(["build"])
|
||||
assert "DEBUG" not in caplog.text
|
||||
|
||||
blag.main(["--verbose", "build"])
|
||||
assert "DEBUG" in caplog.text
|
||||
|
||||
@@ -1,66 +1,82 @@
|
||||
import time
|
||||
"""Tests for the devserver module."""
|
||||
|
||||
|
||||
# remove when we don't support py38 anymore
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
from argparse import Namespace
|
||||
|
||||
import pytest
|
||||
|
||||
from blag import devserver
|
||||
|
||||
|
||||
def test_get_last_modified(cleandir):
|
||||
def test_get_last_modified(cleandir: str) -> None:
|
||||
"""Test get_last_modified."""
|
||||
# take initial time
|
||||
t1 = devserver.get_last_modified(['content'])
|
||||
t1 = devserver.get_last_modified(["content"])
|
||||
|
||||
# wait a bit, create a file and measure again
|
||||
time.sleep(0.1)
|
||||
with open('content/test', 'w') as fh:
|
||||
fh.write('boo')
|
||||
t2 = devserver.get_last_modified(['content'])
|
||||
with open("content/test", "w") as fh:
|
||||
fh.write("boo")
|
||||
t2 = devserver.get_last_modified(["content"])
|
||||
|
||||
# wait a bit and take time again
|
||||
time.sleep(0.1)
|
||||
t3 = devserver.get_last_modified(['content'])
|
||||
t3 = devserver.get_last_modified(["content"])
|
||||
|
||||
assert t2 > t1
|
||||
assert t2 == t3
|
||||
|
||||
|
||||
def test_autoreload_builds_immediately(args):
|
||||
def test_autoreload_builds_immediately(args: Namespace) -> None:
|
||||
"""Test autoreload builds immediately."""
|
||||
# create a dummy file that can be build
|
||||
with open('content/test.md', 'w') as fh:
|
||||
fh.write('boo')
|
||||
with open("content/test.md", "w") as fh:
|
||||
fh.write("boo")
|
||||
|
||||
t = threading.Thread(target=devserver.autoreload,
|
||||
args=(args, ),
|
||||
daemon=True,)
|
||||
t0 = devserver.get_last_modified(['build'])
|
||||
t = threading.Thread(
|
||||
target=devserver.autoreload,
|
||||
args=(args,),
|
||||
daemon=True,
|
||||
)
|
||||
t0 = devserver.get_last_modified(["build"])
|
||||
t.start()
|
||||
# try for 5 seconds...
|
||||
for i in range(5):
|
||||
time.sleep(1)
|
||||
t1 = devserver.get_last_modified(['build'])
|
||||
t1 = devserver.get_last_modified(["build"])
|
||||
print(t1)
|
||||
if t1 > t0:
|
||||
break
|
||||
assert t1 > t0
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::pytest.PytestUnhandledThreadExceptionWarning") # noqa
|
||||
def test_autoreload(args):
|
||||
t = threading.Thread(target=devserver.autoreload,
|
||||
args=(args, ),
|
||||
daemon=True,)
|
||||
@pytest.mark.filterwarnings(
|
||||
"ignore::pytest.PytestUnhandledThreadExceptionWarning"
|
||||
)
|
||||
def test_autoreload(args: Namespace) -> None:
|
||||
"""Test autoreload."""
|
||||
t = threading.Thread(
|
||||
target=devserver.autoreload,
|
||||
args=(args,),
|
||||
daemon=True,
|
||||
)
|
||||
t.start()
|
||||
|
||||
t0 = devserver.get_last_modified(['build'])
|
||||
t0 = devserver.get_last_modified(["build"])
|
||||
|
||||
# create a dummy file that can be build
|
||||
with open('content/test.md', 'w') as fh:
|
||||
fh.write('boo')
|
||||
with open("content/test.md", "w") as fh:
|
||||
fh.write("boo")
|
||||
|
||||
# try for 5 seconds to see if we rebuild once...
|
||||
for i in range(5):
|
||||
time.sleep(1)
|
||||
t1 = devserver.get_last_modified(['build'])
|
||||
t1 = devserver.get_last_modified(["build"])
|
||||
if t1 > t0:
|
||||
break
|
||||
assert t1 > t0
|
||||
|
||||
@@ -1,57 +1,94 @@
|
||||
from datetime import datetime
|
||||
"""Test markdown module."""
|
||||
|
||||
|
||||
# remove when we don't support py38 anymore
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
import markdown
|
||||
import pytest
|
||||
|
||||
from blag.markdown import convert_markdown, markdown_factory
|
||||
|
||||
|
||||
@pytest.mark.parametrize("input_, expected", [
|
||||
# inline
|
||||
('[test](test.md)', 'test.html'),
|
||||
('[test](test.md "test")', 'test.html'),
|
||||
('[test](a/test.md)', 'a/test.html'),
|
||||
('[test](a/test.md "test")', 'a/test.html'),
|
||||
('[test](/test.md)', '/test.html'),
|
||||
('[test](/test.md "test")', '/test.html'),
|
||||
('[test](/a/test.md)', '/a/test.html'),
|
||||
('[test](/a/test.md "test")', '/a/test.html'),
|
||||
# reference
|
||||
('[test][]\n[test]: test.md ''', '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 "test"', 'a/test.html'),
|
||||
('[test][]\n[test]: /test.md', '/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 "test"', '/a/test.html'),
|
||||
])
|
||||
def test_convert_markdown_links(input_, expected):
|
||||
@pytest.mark.parametrize(
|
||||
"input_, expected",
|
||||
[
|
||||
# inline
|
||||
("[test](test.md)", "test.html"),
|
||||
('[test](test.md "test")', "test.html"),
|
||||
("[test](a/test.md)", "a/test.html"),
|
||||
('[test](a/test.md "test")', "a/test.html"),
|
||||
("[test](/test.md)", "/test.html"),
|
||||
('[test](/test.md "test")', "/test.html"),
|
||||
("[test](/a/test.md)", "/a/test.html"),
|
||||
('[test](/a/test.md "test")', "/a/test.html"),
|
||||
# reference
|
||||
("[test][]\n[test]: test.md " "", "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 "test"', "a/test.html"),
|
||||
("[test][]\n[test]: /test.md", "/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 "test"', "/a/test.html"),
|
||||
],
|
||||
)
|
||||
def test_convert_markdown_links(input_: str, expected: str) -> None:
|
||||
"""Test convert_markdown."""
|
||||
md = markdown_factory()
|
||||
html, _ = convert_markdown(md, input_)
|
||||
assert expected in html
|
||||
|
||||
|
||||
@pytest.mark.parametrize("input_, expected", [
|
||||
('foo: bar', {'foo': 'bar'}),
|
||||
('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', {'tags': ['this', 'is', 'a', 'test']}),
|
||||
('date: 2020-01-01 12:10', {'date':
|
||||
datetime(2020, 1, 1, 12, 10).astimezone()}),
|
||||
])
|
||||
def test_convert_metadata(input_, expected):
|
||||
@pytest.mark.parametrize(
|
||||
"input_, expected",
|
||||
[
|
||||
# scheme
|
||||
("[test](https://)", "https://"),
|
||||
# netloc
|
||||
("[test](//test.md)", "//test.md"),
|
||||
# no path
|
||||
("[test]()", ""),
|
||||
],
|
||||
)
|
||||
def test_dont_convert_normal_links(input_: str, expected: str) -> None:
|
||||
"""Test convert_markdown doesn't convert normal links."""
|
||||
md = markdown_factory()
|
||||
html, _ = convert_markdown(md, input_)
|
||||
assert expected in html
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"input_, expected",
|
||||
[
|
||||
("foo: bar", {"foo": "bar"}),
|
||||
("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", {"tags": ["this", "is", "a", "test"]}),
|
||||
(
|
||||
"date: 2020-01-01 12:10",
|
||||
{"date": datetime(2020, 1, 1, 12, 10).astimezone()},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_convert_metadata(input_: str, expected: dict[str, Any]) -> None:
|
||||
"""Test convert_markdown converts metadata correctly."""
|
||||
md = markdown_factory()
|
||||
_, meta = convert_markdown(md, input_)
|
||||
assert expected == meta
|
||||
|
||||
|
||||
def test_markdown_factory():
|
||||
def test_markdown_factory() -> None:
|
||||
"""Test markdown_factory."""
|
||||
md = markdown_factory()
|
||||
assert isinstance(md, markdown.Markdown)
|
||||
|
||||
|
||||
def test_smarty():
|
||||
def test_smarty() -> None:
|
||||
"""Test smarty."""
|
||||
md = markdown_factory()
|
||||
|
||||
md1 = """
|
||||
@@ -60,12 +97,13 @@ this --- is -- a test ...
|
||||
|
||||
"""
|
||||
html, meta = convert_markdown(md, md1)
|
||||
assert 'mdash' in html
|
||||
assert 'ndash' in html
|
||||
assert 'hellip' in html
|
||||
assert "mdash" in html
|
||||
assert "ndash" in html
|
||||
assert "hellip" in html
|
||||
|
||||
|
||||
def test_smarty_code():
|
||||
def test_smarty_code() -> None:
|
||||
"""Test smarty doesn't touch code."""
|
||||
md = markdown_factory()
|
||||
|
||||
md1 = """
|
||||
@@ -74,6 +112,6 @@ this --- is -- a test ...
|
||||
```
|
||||
"""
|
||||
html, meta = convert_markdown(md, md1)
|
||||
assert 'mdash' not in html
|
||||
assert 'ndash' not in html
|
||||
assert 'hellip' not in html
|
||||
assert "mdash" not in html
|
||||
assert "ndash" not in html
|
||||
assert "hellip" not in html
|
||||
|
||||
51
tests/test_quickstart.py
Normal file
51
tests/test_quickstart.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""Tests for the quickstart module."""
|
||||
|
||||
|
||||
# remove when we don't support py38 anymore
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from pytest import MonkeyPatch
|
||||
|
||||
from blag.quickstart import get_input, quickstart
|
||||
|
||||
|
||||
def test_get_input_default_answer(monkeypatch: MonkeyPatch) -> None:
|
||||
"""Test get_input with default answer."""
|
||||
monkeypatch.setattr("builtins.input", lambda x: "")
|
||||
answer = get_input("foo", "bar")
|
||||
assert answer == "bar"
|
||||
|
||||
|
||||
def test_get_input(monkeypatch: MonkeyPatch) -> None:
|
||||
"""Test get_input."""
|
||||
monkeypatch.setattr("builtins.input", lambda x: "baz")
|
||||
answer = get_input("foo", "bar")
|
||||
assert answer == "baz"
|
||||
|
||||
|
||||
def test_quickstart(cleandir: str, monkeypatch: MonkeyPatch) -> None:
|
||||
"""Test quickstart."""
|
||||
monkeypatch.setattr("builtins.input", lambda x: "foo")
|
||||
quickstart(None)
|
||||
with open("config.ini") as fh:
|
||||
data = fh.read()
|
||||
assert "base_url = foo" in data
|
||||
assert "title = foo" in data
|
||||
assert "description = foo" in data
|
||||
assert "author = foo" in data
|
||||
|
||||
for template in (
|
||||
"archive.html",
|
||||
"article.html",
|
||||
"base.html",
|
||||
"index.html",
|
||||
"page.html",
|
||||
"tag.html",
|
||||
"tags.html",
|
||||
):
|
||||
assert os.path.exists(f"templates/{template}")
|
||||
|
||||
for directory in "build", "content", "static":
|
||||
assert os.path.exists(directory)
|
||||
@@ -1,71 +1,107 @@
|
||||
"""Test the templates."""
|
||||
|
||||
|
||||
# remove when we don't support py38 anymore
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
|
||||
from jinja2 import Template
|
||||
|
||||
def test_page(page_template):
|
||||
|
||||
def test_page(page_template: Template) -> None:
|
||||
"""Test the page template."""
|
||||
ctx = {
|
||||
'content': 'this is the content',
|
||||
'title': 'this is the title',
|
||||
"content": "this is the content",
|
||||
"title": "this is the title",
|
||||
}
|
||||
result = page_template.render(ctx)
|
||||
assert 'this is the content' in result
|
||||
assert 'this is the title' in result
|
||||
assert "this is the content" in result
|
||||
assert "this is the title" in result
|
||||
|
||||
|
||||
def test_article(article_template):
|
||||
def test_article(article_template: Template) -> None:
|
||||
"""Test the article template."""
|
||||
ctx = {
|
||||
'content': 'this is the content',
|
||||
'title': 'this is the title',
|
||||
"content": "this is the content",
|
||||
"title": "this is the title",
|
||||
"date": datetime.datetime(1980, 5, 9),
|
||||
}
|
||||
result = article_template.render(ctx)
|
||||
assert 'this is the content' in result
|
||||
assert 'this is the title' in result
|
||||
assert "this is the content" in result
|
||||
assert "this is the title" in result
|
||||
assert "1980-05-09" in result
|
||||
|
||||
|
||||
def test_archive(archive_template):
|
||||
def test_index(index_template: Template) -> None:
|
||||
"""Test the index template."""
|
||||
entry = {
|
||||
'title': 'this is a title',
|
||||
'dst': 'https://example.com/link',
|
||||
'date': datetime.datetime(1980, 5, 9),
|
||||
"title": "this is a title",
|
||||
"dst": "https://example.com/link",
|
||||
"date": datetime.datetime(1980, 5, 9),
|
||||
}
|
||||
archive = [entry]
|
||||
ctx = {
|
||||
'archive': archive,
|
||||
"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:
|
||||
"""Test the archive template."""
|
||||
entry = {
|
||||
"title": "this is a title",
|
||||
"dst": "https://example.com/link",
|
||||
"date": datetime.datetime(1980, 5, 9),
|
||||
}
|
||||
archive = [entry]
|
||||
ctx = {
|
||||
"archive": archive,
|
||||
}
|
||||
result = archive_template.render(ctx)
|
||||
assert 'site title' in result
|
||||
assert "Archive" in result
|
||||
|
||||
assert 'this is a title' in result
|
||||
assert '1980-05-09' in result
|
||||
assert 'https://example.com/link' in result
|
||||
assert "this is a title" in result
|
||||
assert "1980-05-09" in result
|
||||
assert "https://example.com/link" in result
|
||||
|
||||
|
||||
def test_tags(tags_template):
|
||||
tags = [('foo', 42)]
|
||||
def test_tags(tags_template: Template) -> None:
|
||||
"""Test the tags template."""
|
||||
tags = [("foo", 42)]
|
||||
ctx = {
|
||||
'tags': tags,
|
||||
"tags": tags,
|
||||
}
|
||||
result = tags_template.render(ctx)
|
||||
assert 'Tags' in result
|
||||
assert "Tags" in result
|
||||
|
||||
assert 'foo.html' in result
|
||||
assert 'foo' in result
|
||||
assert '42' in result
|
||||
assert "foo.html" in result
|
||||
assert "foo" in result
|
||||
assert "42" in result
|
||||
|
||||
|
||||
def test_tag(tag_template):
|
||||
def test_tag(tag_template: Template) -> None:
|
||||
"""Test the tag template."""
|
||||
entry = {
|
||||
'title': 'this is a title',
|
||||
'dst': 'https://example.com/link',
|
||||
'date': datetime.datetime(1980, 5, 9),
|
||||
"title": "this is a title",
|
||||
"dst": "https://example.com/link",
|
||||
"date": datetime.datetime(1980, 5, 9),
|
||||
}
|
||||
archive = [entry]
|
||||
ctx = {
|
||||
'tag': 'foo',
|
||||
'archive': archive,
|
||||
"tag": "foo",
|
||||
"archive": archive,
|
||||
}
|
||||
result = tag_template.render(ctx)
|
||||
assert 'Tag foo' in result
|
||||
assert "foo" in result
|
||||
|
||||
assert 'this is a title' in result
|
||||
assert '1980-05-09' in result
|
||||
assert 'https://example.com/link' in result
|
||||
assert "this is a title" in result
|
||||
assert "1980-05-09" in result
|
||||
assert "https://example.com/link" in result
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
"""Test the version module."""
|
||||
|
||||
|
||||
# remove when we don't support py38 anymore
|
||||
from __future__ import annotations
|
||||
|
||||
import blag
|
||||
|
||||
|
||||
def test_version():
|
||||
def test_version() -> None:
|
||||
"""Test the version of the package."""
|
||||
assert isinstance(blag.__VERSION__, str)
|
||||
|
||||
Reference in New Issue
Block a user