forked from github.com/blag
Compare commits
223 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
7e8f2a5b9a | ||
|
|
d942bf150c | ||
|
|
48cfb49acb | ||
|
|
c3edbeb511 | ||
|
|
a60887e0d6 | ||
|
|
bfbedcc3df | ||
|
|
dff60d7399 | ||
|
|
f9b6afa80a | ||
|
|
1e74596101 | ||
|
|
c469b9b591 | ||
|
|
d76a0834e3 | ||
|
|
20b1e281a1 | ||
|
|
58e74f8d55 | ||
|
|
aad5f288af | ||
|
|
dc76295203 | ||
|
|
8a9a8cd2eb | ||
|
|
55e82393b6 | ||
|
|
b74cea8296 | ||
|
|
58a164899c | ||
|
|
769dcca83a | ||
|
|
f8bcaafc30 | ||
|
|
f1fe211ac6 | ||
|
|
c88628350f | ||
|
|
60e8b98232 | ||
|
|
b74c34839f | ||
|
|
2a8f93147f | ||
|
|
4c12ef738c | ||
|
|
bc71f51443 | ||
|
|
4cfbdd5108 | ||
|
|
6a07b19eda | ||
|
|
d486b7a90b | ||
|
|
2355799aaa | ||
|
|
788c07446d | ||
|
|
c1375a1478 | ||
|
|
67d9a31256 | ||
|
|
25c6a4c089 | ||
|
|
e75fd4eacb | ||
|
|
88905db579 | ||
|
|
702bc2e986 | ||
|
|
7cc4d8be45 | ||
|
|
764317aa24 | ||
|
|
a8a976403f | ||
|
|
fd9a6e6fa2 | ||
|
|
8f02c107e2 | ||
|
|
489e546173 | ||
|
|
9143a4dc7f | ||
|
|
512c12eaae | ||
|
|
ab3eaf934d | ||
|
|
db30fe1d06 | ||
|
|
27a760d834 | ||
|
|
a9abcd753a | ||
|
|
3deb62ed88 | ||
|
|
2366a2ae86 | ||
|
|
0fb01e3249 | ||
|
|
78316725cf | ||
|
|
4f1632e3cd | ||
|
|
d23f3666dc | ||
|
|
60cfd0290a | ||
|
|
10ea8df1ac | ||
|
|
edc89581af | ||
|
|
6d75891ace | ||
|
|
6367f5a55a | ||
|
|
3cd316a537 | ||
|
|
3b8d2fe9d6 | ||
|
|
1fe576a771 | ||
|
|
2f4d2267a0 | ||
|
|
7bb51ff060 | ||
|
|
5a8012d62b | ||
|
|
713b53d0d9 | ||
|
|
2d1d0ab302 | ||
|
|
572710fab1 | ||
|
|
0b66c5fbf4 | ||
|
|
18ecd82c5a | ||
|
|
a74f36be8a | ||
|
|
dd7d6cdae2 | ||
|
|
39e6aa2676 | ||
|
|
93f6840168 | ||
|
|
e27d82ac2a | ||
|
|
88b4fc8233 | ||
|
|
8923b0bb6e | ||
|
|
2671239ac1 | ||
|
|
7aa6cebb63 | ||
|
|
4869ea0fd2 | ||
|
|
aeedd9b73a | ||
|
|
52412b8926 | ||
|
|
c0dae31b60 | ||
|
|
2366ee2def | ||
|
|
67f24642e5 | ||
|
|
f1020637e6 | ||
|
|
a4d596b79d | ||
|
|
79edd04ee8 | ||
|
|
9cdecdccf7 | ||
|
|
499b0dfe11 | ||
|
|
72971408b2 | ||
|
|
6445f31204 | ||
|
|
98b97fbbbd | ||
|
|
df65dee488 | ||
|
|
d227392c79 | ||
|
|
03663a855d | ||
|
|
0d2c54071e | ||
|
|
cffb4cf49d | ||
|
|
0d4052dbb5 | ||
|
|
eada12097d | ||
|
|
4c14cac499 | ||
|
|
637d57eb85 | ||
|
|
746841f05c | ||
|
|
a78d4238b6 | ||
|
|
a8e14e86d0 | ||
|
|
cdc6639447 | ||
|
|
9c228165e9 | ||
|
|
2b3651e7d5 | ||
|
|
7240b0a28b | ||
|
|
3c264966c0 | ||
|
|
b24241544b | ||
|
|
9a96fe5e81 | ||
|
|
c60c98c37c | ||
|
|
179100005c | ||
|
|
b2f32e84e4 | ||
|
|
14ebc2769c | ||
|
|
e2ebd950ae | ||
|
|
576121afac | ||
|
|
2d3bb0c0f3 | ||
|
|
01b203ff5c | ||
|
|
6f70f7ca93 | ||
|
|
7f832a1445 | ||
|
|
aac2d70fed | ||
|
|
dc6547290b | ||
|
|
b077e22984 | ||
|
|
af5825b412 | ||
|
|
7d69c37032 | ||
|
|
dbd1679038 | ||
|
|
7eafaba49a | ||
|
|
a98b2071fd | ||
|
|
98e124dfc1 | ||
|
|
3fe9a1ae16 | ||
|
|
12c3315808 | ||
|
|
7decb8fddd | ||
|
|
7cb373af94 | ||
|
|
65fdb3405a | ||
|
|
6a57641ec2 | ||
|
|
96e2eb76d4 | ||
|
|
59d7d2bb71 |
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "pip"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
71
.github/workflows/python-package.yaml
vendored
71
.github/workflows/python-package.yaml
vendored
@@ -17,26 +17,65 @@ jobs:
|
|||||||
- macos-latest
|
- macos-latest
|
||||||
- windows-latest
|
- windows-latest
|
||||||
python-version:
|
python-version:
|
||||||
- 3.8
|
- "3.8"
|
||||||
- 3.9
|
- "3.9"
|
||||||
|
- "3.10"
|
||||||
|
- "3.11"
|
||||||
|
exclude:
|
||||||
|
# 3.8 on windows fails due to some pip issue
|
||||||
|
- os: windows-latest
|
||||||
|
python-version: "3.8"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-python@v4
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- run: |
|
||||||
run: |
|
make venv
|
||||||
pip install -r requirements.txt
|
- run: |
|
||||||
pip install -r requirements-dev.txt
|
make test
|
||||||
|
|
||||||
- name: Run tests
|
lint:
|
||||||
run: |
|
runs-on: ubuntu-latest
|
||||||
pytest
|
|
||||||
|
|
||||||
- name: Run linter
|
steps:
|
||||||
run: |
|
- uses: actions/checkout@v3
|
||||||
flake8
|
- 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@v3
|
||||||
|
- 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@v3
|
||||||
|
- uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
make venv
|
||||||
|
- run: |
|
||||||
|
make test-release
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,7 +5,11 @@ build/
|
|||||||
dist/
|
dist/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
|
|
||||||
|
docs/_build/
|
||||||
|
docs/api/
|
||||||
|
|
||||||
htmlcov/
|
htmlcov/
|
||||||
.coverage
|
.coverage
|
||||||
|
.mypy_cache
|
||||||
|
|
||||||
venv/
|
venv/
|
||||||
|
|||||||
8
.readthedocs.yaml
Normal file
8
.readthedocs.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
python:
|
||||||
|
version: 3.8
|
||||||
|
install:
|
||||||
|
- requirements: requirements.txt
|
||||||
|
- requirements: requirements-dev.txt
|
||||||
|
- path: .
|
||||||
96
CHANGELOG.md
96
CHANGELOG.md
@@ -1,6 +1,98 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [0.0.0] - YYYY-MM-DD
|
## [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
|
||||||
|
|
||||||
|
## [1.3.0] - 2022-06-09
|
||||||
|
|
||||||
|
* debianized package
|
||||||
|
* Small fix in makefile
|
||||||
|
* updated dependencies:
|
||||||
|
* pytest 7.1.2
|
||||||
|
* sphinx 5.0.0
|
||||||
|
* twine 3.7.1
|
||||||
|
* wheel 0.37.1
|
||||||
|
* markdown 3.3.7
|
||||||
|
* jinja 3.1.2
|
||||||
|
* pygments 2.12.0
|
||||||
|
|
||||||
|
## [1.2.0] - 2021-11-06
|
||||||
|
|
||||||
|
* `make serve` now rebuilds immediately once after called to avoid serving
|
||||||
|
stale files
|
||||||
|
* updated dependencies:
|
||||||
|
* feedgenerator 2.0.0
|
||||||
|
* jinja2 3.0.1
|
||||||
|
* pytest-cov 3.0.0
|
||||||
|
* flake8 4.0.1
|
||||||
|
* twine 3.5.0
|
||||||
|
|
||||||
|
## [1.1.0] - 2021-10-06
|
||||||
|
|
||||||
|
* added Python 3.10 to list of supported versions to test against
|
||||||
|
* added dependabot to github workflows
|
||||||
|
* updated various dependencies:
|
||||||
|
* pygments 2.10.0
|
||||||
|
* sphinx 4.2.0
|
||||||
|
* twine 3.4.2
|
||||||
|
* wheel 0.37.0
|
||||||
|
* pytest 6.2.5
|
||||||
|
|
||||||
|
## [1.0.0] - 2021-08-18
|
||||||
|
|
||||||
|
* first 1.0 release!
|
||||||
|
* bump requirements of feedgenerator to 1.9.2. this version uses the
|
||||||
|
description to provide a subtitle for the feed
|
||||||
|
|
||||||
|
## [0.0.9] - 2021-06-22
|
||||||
|
|
||||||
|
* updated to jinja 3.0
|
||||||
|
* updated to sphinx 4.0
|
||||||
|
* added link to changelog
|
||||||
|
|||||||
58
Makefile
58
Makefile
@@ -1,39 +1,65 @@
|
|||||||
|
# system python interpreter. used only to create virtual environment
|
||||||
|
PY = python3
|
||||||
VENV = venv
|
VENV = venv
|
||||||
|
BIN=$(VENV)/bin
|
||||||
|
|
||||||
|
DOCS_SRC = docs
|
||||||
|
DOCS_OUT = $(DOCS_SRC)/_build
|
||||||
|
|
||||||
|
|
||||||
ifeq ($(OS), Windows_NT)
|
ifeq ($(OS), Windows_NT)
|
||||||
BIN=$(VENV)/Scripts
|
BIN=$(VENV)/Scripts
|
||||||
else
|
PY=python
|
||||||
BIN=$(VENV)/bin
|
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
|
||||||
all: lint test
|
.PHONY: all
|
||||||
|
all: lint mypy test test-release
|
||||||
|
|
||||||
$(VENV): requirements.txt requirements-dev.txt setup.py
|
$(VENV): requirements.txt requirements-dev.txt pyproject.toml
|
||||||
python3 -m venv $(VENV)
|
$(PY) -m venv $(VENV)
|
||||||
$(BIN)/python3 -m pip install --upgrade -r requirements.txt
|
$(BIN)/pip install --upgrade -r requirements.txt
|
||||||
$(BIN)/python3 -m pip install --upgrade -r requirements-dev.txt
|
$(BIN)/pip install --upgrade -r requirements-dev.txt
|
||||||
$(BIN)/python3 -m pip install -e .
|
$(BIN)/pip install -e .['dev']
|
||||||
touch $(VENV)
|
touch $(VENV)
|
||||||
|
|
||||||
test: $(VENV)
|
|
||||||
$(BIN)/python3 -m pytest
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
|
test: $(VENV)
|
||||||
|
$(BIN)/pytest
|
||||||
|
|
||||||
|
.PHONY: mypy
|
||||||
|
mypy: $(VENV)
|
||||||
|
$(BIN)/mypy
|
||||||
|
|
||||||
lint: $(VENV)
|
|
||||||
$(BIN)/python3 -m flake8
|
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
|
lint: $(VENV)
|
||||||
|
$(BIN)/flake8
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build: $(VENV)
|
||||||
|
rm -rf dist
|
||||||
|
$(BIN)/python3 -m build
|
||||||
|
|
||||||
|
.PHONY: test-release
|
||||||
|
test-release: $(VENV) build
|
||||||
|
$(BIN)/twine check dist/*
|
||||||
|
|
||||||
release: $(VENV)
|
|
||||||
$(BIN)/python3 setup.py sdist bdist_wheel
|
|
||||||
$(BIN)/twine upload dist/*
|
|
||||||
.PHONY: release
|
.PHONY: release
|
||||||
|
release: $(VENV) build
|
||||||
|
$(BIN)/twine upload dist/*
|
||||||
|
|
||||||
|
.PHONY: docs
|
||||||
|
docs: $(VENV)
|
||||||
|
$(BIN)/sphinx-build $(DOCS_SRC) $(DOCS_OUT)
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
rm -rf build dist *.egg-info
|
rm -rf build dist *.egg-info
|
||||||
rm -rf $(VENV)
|
rm -rf $(VENV)
|
||||||
|
rm -rf $(DOCS_OUT)
|
||||||
|
rm -rf $(DOCS_SRC)/api
|
||||||
find . -type f -name *.pyc -delete
|
find . -type f -name *.pyc -delete
|
||||||
find . -type d -name __pycache__ -delete
|
find . -type d -name __pycache__ -delete
|
||||||
# coverage
|
# coverage
|
||||||
rm -rf htmlcov .coverage
|
rm -rf htmlcov .coverage
|
||||||
.PHONY: clean
|
rm -rf .mypy_cache
|
||||||
|
|||||||
52
README.md
52
README.md
@@ -1,11 +1,55 @@
|
|||||||
# blag -- a simple, blog-aware static site generator
|
# blag
|
||||||
|
|
||||||
## Installation
|
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][]
|
||||||
|
* 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
|
```bash
|
||||||
$ pip install blag
|
$ pip install blag
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
On Debian or Ubuntu, you can also just install the Debian package:
|
||||||
|
|
||||||
TBD
|
```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 +1 @@
|
|||||||
from blag.version import __VERSION__ # noqa
|
from blag.version import __VERSION__ as __VERSION__ # noqa
|
||||||
|
|||||||
332
blag/blag.py
332
blag/blag.py
@@ -1,13 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
"""Small static site generator.
|
"""blag's core methods.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# remove when we don't support py38 anymore
|
||||||
__author__ = "Bastian Venthur <venthur@debian.org>"
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
@@ -15,71 +14,159 @@ import logging
|
|||||||
import configparser
|
import configparser
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from jinja2 import Environment, ChoiceLoader, FileSystemLoader, PackageLoader
|
from jinja2 import (
|
||||||
|
Environment,
|
||||||
|
ChoiceLoader,
|
||||||
|
FileSystemLoader,
|
||||||
|
PackageLoader,
|
||||||
|
Template,
|
||||||
|
)
|
||||||
import feedgenerator
|
import feedgenerator
|
||||||
|
|
||||||
from blag.markdown import markdown_factory, convert_markdown
|
from blag.markdown import markdown_factory, convert_markdown
|
||||||
|
from blag.devserver import serve
|
||||||
|
from blag.version import __VERSION__
|
||||||
|
from blag.quickstart import quickstart
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.DEBUG,
|
level=logging.INFO,
|
||||||
format='%(asctime)s %(levelname)s %(name)s %(message)s',
|
format='%(asctime)s %(levelname)s %(name)s %(message)s',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def main(args=None):
|
def main(arguments: list[str] | None = None) -> None:
|
||||||
args = parse_args(args)
|
"""Main entrypoint for the CLI.
|
||||||
|
|
||||||
|
This method parses the CLI arguments and executes the respective
|
||||||
|
commands.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
arguments
|
||||||
|
optional parameters, used for testing
|
||||||
|
|
||||||
|
"""
|
||||||
|
args = parse_args(arguments)
|
||||||
|
# set loglevel
|
||||||
|
if args.verbose:
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
logger.debug(f"This is blag {__VERSION__}.")
|
||||||
args.func(args)
|
args.func(args)
|
||||||
|
|
||||||
|
|
||||||
def parse_args(args=None):
|
def parse_args(args: list[str] | None = None) -> argparse.Namespace:
|
||||||
"""Parse command line arguments.
|
"""Parse command line arguments.
|
||||||
|
|
||||||
Paramters
|
Parameters
|
||||||
---------
|
----------
|
||||||
args : List[str]
|
args
|
||||||
optional parameters, used for testing
|
optional parameters, used for testing
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
args
|
arparse.Namespace
|
||||||
|
|
||||||
"""
|
"""
|
||||||
parser = argparse.ArgumentParser()
|
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
|
commands.required = True
|
||||||
|
|
||||||
build_parser = commands.add_parser('build')
|
build_parser = commands.add_parser(
|
||||||
|
'build',
|
||||||
|
help='Build website.',
|
||||||
|
)
|
||||||
build_parser.set_defaults(func=build)
|
build_parser.set_defaults(func=build)
|
||||||
build_parser.add_argument(
|
build_parser.add_argument(
|
||||||
'-i', '--input-dir',
|
'-i',
|
||||||
default='content',
|
'--input-dir',
|
||||||
help='Input directory (default: content)',
|
default='content',
|
||||||
|
help='Input directory (default: content)',
|
||||||
)
|
)
|
||||||
build_parser.add_argument(
|
build_parser.add_argument(
|
||||||
'-o', '--output-dir',
|
'-o',
|
||||||
default='build',
|
'--output-dir',
|
||||||
help='Ouptut directory (default: build)',
|
default='build',
|
||||||
|
help='Ouptut directory (default: build)',
|
||||||
)
|
)
|
||||||
build_parser.add_argument(
|
build_parser.add_argument(
|
||||||
'-t', '--template-dir',
|
'-t',
|
||||||
default='templates',
|
'--template-dir',
|
||||||
help='Template directory (default: templates)',
|
default='templates',
|
||||||
|
help='Template directory (default: templates)',
|
||||||
)
|
)
|
||||||
build_parser.add_argument(
|
build_parser.add_argument(
|
||||||
'-s', '--static-dir',
|
'-s',
|
||||||
default='static',
|
'--static-dir',
|
||||||
help='Static directory (default: static)',
|
default='static',
|
||||||
|
help='Static directory (default: static)',
|
||||||
)
|
)
|
||||||
|
|
||||||
quickstart_parser = commands.add_parser('quickstart')
|
quickstart_parser = commands.add_parser(
|
||||||
|
'quickstart',
|
||||||
|
help="Quickstart blag, creating necessary configuration.",
|
||||||
|
)
|
||||||
quickstart_parser.set_defaults(func=quickstart)
|
quickstart_parser.set_defaults(func=quickstart)
|
||||||
|
|
||||||
|
serve_parser = commands.add_parser(
|
||||||
|
'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)',
|
||||||
|
)
|
||||||
|
serve_parser.add_argument(
|
||||||
|
'-o',
|
||||||
|
'--output-dir',
|
||||||
|
default='build',
|
||||||
|
help='Ouptut directory (default: build)',
|
||||||
|
)
|
||||||
|
serve_parser.add_argument(
|
||||||
|
'-t',
|
||||||
|
'--template-dir',
|
||||||
|
default='templates',
|
||||||
|
help='Template directory (default: templates)',
|
||||||
|
)
|
||||||
|
serve_parser.add_argument(
|
||||||
|
'-s',
|
||||||
|
'--static-dir',
|
||||||
|
default='static',
|
||||||
|
help='Static directory (default: static)',
|
||||||
|
)
|
||||||
|
|
||||||
return parser.parse_args(args)
|
return parser.parse_args(args)
|
||||||
|
|
||||||
|
|
||||||
def get_config(configfile):
|
def get_config(configfile: str) -> configparser.SectionProxy:
|
||||||
|
"""Load site configuration from configfile.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
configfile
|
||||||
|
path to configuration file
|
||||||
|
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
configparser.SectionProxy
|
||||||
|
|
||||||
|
"""
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
config.read(configfile)
|
config.read(configfile)
|
||||||
# check for the mandatory options
|
# check for the mandatory options
|
||||||
@@ -97,7 +184,10 @@ def get_config(configfile):
|
|||||||
return config['main']
|
return config['main']
|
||||||
|
|
||||||
|
|
||||||
def environment_factory(template_dir=None, globals_=None):
|
def environment_factory(
|
||||||
|
template_dir: str | None = None,
|
||||||
|
globals_: dict[str, object] | None = None,
|
||||||
|
) -> Environment:
|
||||||
"""Environment factory.
|
"""Environment factory.
|
||||||
|
|
||||||
Creates a Jinja2 Environment with the default templates and
|
Creates a Jinja2 Environment with the default templates and
|
||||||
@@ -107,8 +197,9 @@ def environment_factory(template_dir=None, globals_=None):
|
|||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
template_dir : str
|
template_dir
|
||||||
globals_ : dict
|
directory containing the templates
|
||||||
|
globals_
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -117,7 +208,7 @@ def environment_factory(template_dir=None, globals_=None):
|
|||||||
"""
|
"""
|
||||||
# first we try the custom templates, and fall back the ones provided
|
# first we try the custom templates, and fall back the ones provided
|
||||||
# by blag
|
# by blag
|
||||||
loaders = []
|
loaders: list[FileSystemLoader | PackageLoader] = []
|
||||||
if template_dir:
|
if template_dir:
|
||||||
loaders.append(FileSystemLoader([template_dir]))
|
loaders.append(FileSystemLoader([template_dir]))
|
||||||
loaders.append(PackageLoader('blag', 'templates'))
|
loaders.append(PackageLoader('blag', 'templates'))
|
||||||
@@ -127,13 +218,24 @@ def environment_factory(template_dir=None, globals_=None):
|
|||||||
return env
|
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
|
||||||
|
etc.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
args
|
||||||
|
|
||||||
|
"""
|
||||||
os.makedirs(f'{args.output_dir}', exist_ok=True)
|
os.makedirs(f'{args.output_dir}', exist_ok=True)
|
||||||
convertibles = []
|
convertibles = []
|
||||||
for root, dirnames, filenames in os.walk(args.input_dir):
|
for root, dirnames, filenames in os.walk(args.input_dir):
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
rel_src = os.path.relpath(f'{root}/{filename}',
|
rel_src = os.path.relpath(
|
||||||
start=args.input_dir)
|
f'{root}/{filename}', start=args.input_dir
|
||||||
|
)
|
||||||
# all non-markdown files are just copied over, the markdown
|
# all non-markdown files are just copied over, the markdown
|
||||||
# files are converted to html
|
# files are converted to html
|
||||||
if rel_src.endswith('.md'):
|
if rel_src.endswith('.md'):
|
||||||
@@ -141,14 +243,17 @@ def build(args):
|
|||||||
rel_dst = rel_dst[:-3] + '.html'
|
rel_dst = rel_dst[:-3] + '.html'
|
||||||
convertibles.append((rel_src, rel_dst))
|
convertibles.append((rel_src, rel_dst))
|
||||||
else:
|
else:
|
||||||
shutil.copy(f'{args.input_dir}/{rel_src}',
|
shutil.copy(
|
||||||
f'{args.output_dir}/{rel_src}')
|
f'{args.input_dir}/{rel_src}',
|
||||||
|
f'{args.output_dir}/{rel_src}',
|
||||||
|
)
|
||||||
for dirname in dirnames:
|
for dirname in dirnames:
|
||||||
# all directories are copied into the output directory
|
# all directories are copied into the output directory
|
||||||
path = os.path.relpath(f'{root}/{dirname}', start=args.input_dir)
|
path = os.path.relpath(f'{root}/{dirname}', start=args.input_dir)
|
||||||
os.makedirs(f'{args.output_dir}/{path}', exist_ok=True)
|
os.makedirs(f'{args.output_dir}/{path}', exist_ok=True)
|
||||||
|
|
||||||
# copy static files over
|
# copy static files over
|
||||||
|
logger.info('Copying static files.')
|
||||||
if os.path.exists(args.static_dir):
|
if os.path.exists(args.static_dir):
|
||||||
shutil.copytree(args.static_dir, args.output_dir, dirs_exist_ok=True)
|
shutil.copytree(args.static_dir, args.output_dir, dirs_exist_ok=True)
|
||||||
|
|
||||||
@@ -171,7 +276,8 @@ def build(args):
|
|||||||
)
|
)
|
||||||
|
|
||||||
generate_feed(
|
generate_feed(
|
||||||
articles, args.output_dir,
|
articles,
|
||||||
|
args.output_dir,
|
||||||
base_url=config['base_url'],
|
base_url=config['base_url'],
|
||||||
blog_title=config['title'],
|
blog_title=config['title'],
|
||||||
blog_description=config['description'],
|
blog_description=config['description'],
|
||||||
@@ -181,8 +287,13 @@ def build(args):
|
|||||||
generate_tags(articles, tags_template, tag_template, args.output_dir)
|
generate_tags(articles, tags_template, tag_template, args.output_dir)
|
||||||
|
|
||||||
|
|
||||||
def process_markdown(convertibles, input_dir, output_dir,
|
def process_markdown(
|
||||||
page_template, article_template):
|
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.
|
"""Process markdown files.
|
||||||
|
|
||||||
This method processes the convertibles, converts them to html and
|
This method processes the convertibles, converts them to html and
|
||||||
@@ -193,24 +304,27 @@ def process_markdown(convertibles, input_dir, output_dir,
|
|||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
convertibles : List[Tuple[str, str]]
|
convertibles
|
||||||
relative paths to markdown- (src) html- (dest) files
|
relative paths to markdown- (src) html- (dest) files
|
||||||
input_dir : str
|
input_dir
|
||||||
output_dir : str
|
output_dir
|
||||||
page_template, archive_template : jinja2 template
|
page_template, archive_template
|
||||||
templats for pages and articles
|
templats for pages and articles
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
articles, pages : List[Tuple[str, Dict]]
|
list[tuple[str, dict[str, Any]]], list[tuple[str, dict[str, Any]]]
|
||||||
|
articles and pages
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
logger.info("Converting Markdown files...")
|
||||||
md = markdown_factory()
|
md = markdown_factory()
|
||||||
|
|
||||||
articles = []
|
articles = []
|
||||||
pages = []
|
pages = []
|
||||||
for src, dst in convertibles:
|
for src, dst in convertibles:
|
||||||
logger.debug(f'Processing {src}')
|
logger.debug(f'Processing {src}')
|
||||||
|
|
||||||
with open(f'{input_dir}/{src}', 'r') as fh:
|
with open(f'{input_dir}/{src}', 'r') as fh:
|
||||||
body = fh.read()
|
body = fh.read()
|
||||||
|
|
||||||
@@ -225,7 +339,7 @@ def process_markdown(convertibles, input_dir, output_dir,
|
|||||||
articles.append((dst, context))
|
articles.append((dst, context))
|
||||||
result = article_template.render(context)
|
result = article_template.render(context)
|
||||||
else:
|
else:
|
||||||
pages.append((dst, content))
|
pages.append((dst, context))
|
||||||
result = page_template.render(context)
|
result = page_template.render(context)
|
||||||
with open(f'{output_dir}/{dst}', 'w') as fh_dest:
|
with open(f'{output_dir}/{dst}', 'w') as fh_dest:
|
||||||
fh_dest.write(result)
|
fh_dest.write(result)
|
||||||
@@ -236,26 +350,49 @@ def process_markdown(convertibles, input_dir, output_dir,
|
|||||||
|
|
||||||
|
|
||||||
def generate_feed(
|
def generate_feed(
|
||||||
articles,
|
articles: list[tuple[str, dict[str, Any]]],
|
||||||
output_dir,
|
output_dir: str,
|
||||||
base_url,
|
base_url: str,
|
||||||
blog_title,
|
blog_title: str,
|
||||||
blog_description,
|
blog_description: str,
|
||||||
blog_author,
|
blog_author: str,
|
||||||
):
|
) -> None:
|
||||||
|
"""Generate Atom feed.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
articles
|
||||||
|
list of relative output path and article dictionary
|
||||||
|
output_dir
|
||||||
|
where the feed is stored
|
||||||
|
base_url
|
||||||
|
base url
|
||||||
|
blog_title
|
||||||
|
blog title
|
||||||
|
blog_description
|
||||||
|
blog description
|
||||||
|
blog_author
|
||||||
|
blog author
|
||||||
|
|
||||||
|
"""
|
||||||
|
logger.info('Generating Atom feed.')
|
||||||
feed = feedgenerator.Atom1Feed(
|
feed = feedgenerator.Atom1Feed(
|
||||||
link=base_url,
|
link=base_url,
|
||||||
title=blog_title,
|
title=blog_title,
|
||||||
description=blog_description,
|
description=blog_description,
|
||||||
feed_url=base_url + 'atom.xml',
|
feed_url=base_url + 'atom.xml',
|
||||||
)
|
)
|
||||||
|
|
||||||
for dst, context in articles:
|
for dst, context in articles:
|
||||||
|
# if article has a description, use that. otherwise fall back to
|
||||||
|
# the title
|
||||||
|
description = context.get('description', context['title'])
|
||||||
|
|
||||||
feed.add_item(
|
feed.add_item(
|
||||||
title=context['title'],
|
title=context['title'],
|
||||||
author_name=blog_author,
|
author_name=blog_author,
|
||||||
link=base_url + dst,
|
link=base_url + dst,
|
||||||
description=context['title'],
|
description=description,
|
||||||
content=context['content'],
|
content=context['content'],
|
||||||
pubdate=context['date'],
|
pubdate=context['date'],
|
||||||
)
|
)
|
||||||
@@ -264,7 +401,22 @@ def generate_feed(
|
|||||||
feed.write(fh, encoding='utf8')
|
feed.write(fh, encoding='utf8')
|
||||||
|
|
||||||
|
|
||||||
def generate_archive(articles, template, output_dir):
|
def generate_archive(
|
||||||
|
articles: list[tuple[str, dict[str, Any]]],
|
||||||
|
template: Template,
|
||||||
|
output_dir: str,
|
||||||
|
) -> None:
|
||||||
|
"""Generate the archive page.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
articles
|
||||||
|
List of articles. Each article has the destination path and a
|
||||||
|
dictionary with the content.
|
||||||
|
template
|
||||||
|
output_dir
|
||||||
|
|
||||||
|
"""
|
||||||
archive = []
|
archive = []
|
||||||
for dst, context in articles:
|
for dst, context in articles:
|
||||||
entry = context.copy()
|
entry = context.copy()
|
||||||
@@ -276,56 +428,56 @@ def generate_archive(articles, template, output_dir):
|
|||||||
fh.write(result)
|
fh.write(result)
|
||||||
|
|
||||||
|
|
||||||
def generate_tags(articles, tags_template, tag_template, output_dir):
|
def generate_tags(
|
||||||
os.makedirs(f'{output_dir}/tags', exist_ok=True)
|
articles: list[tuple[str, dict[str, Any]]],
|
||||||
|
tags_template: Template,
|
||||||
|
tag_template: Template,
|
||||||
|
output_dir: str,
|
||||||
|
) -> None:
|
||||||
|
"""Generate the tags page.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
articles
|
||||||
|
List of articles. Each article has the destination path and a
|
||||||
|
dictionary with the content.
|
||||||
|
tags_template, tag_template
|
||||||
|
output_dir
|
||||||
|
|
||||||
|
"""
|
||||||
|
logger.info("Generating Tag-pages.")
|
||||||
|
os.makedirs(f'{output_dir}/tags', exist_ok=True)
|
||||||
# get tags number of occurrences
|
# get tags number of occurrences
|
||||||
all_tags = {}
|
all_tags: dict[str, int] = {}
|
||||||
for _, context in articles:
|
for _, context in articles:
|
||||||
tags = context.get('tags', None)
|
tags: list[str] = context.get('tags', [])
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
all_tags[tag] = all_tags.get(tag, 0) + 1
|
all_tags[tag] = all_tags.get(tag, 0) + 1
|
||||||
# sort by occurrence
|
# sort by occurrence
|
||||||
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))
|
result = tags_template.render(dict(tags=taglist))
|
||||||
with open(f'{output_dir}/tags/index.html', 'w') as fh:
|
with open(f'{output_dir}/tags/index.html', 'w') as fh:
|
||||||
fh.write(result)
|
fh.write(result)
|
||||||
|
|
||||||
# get tags and archive per tag
|
# get tags and archive per tag
|
||||||
all_tags = {}
|
all_tags2: dict[str, list[dict[str, Any]]] = {}
|
||||||
for dst, context in articles:
|
for dst, context in articles:
|
||||||
tags = context.get('tags', None)
|
tags = context.get('tags', [])
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
archive = all_tags.get(tag, [])
|
archive: list[dict[str, Any]] = all_tags2.get(tag, [])
|
||||||
entry = context.copy()
|
entry = context.copy()
|
||||||
entry['dst'] = dst
|
entry['dst'] = dst
|
||||||
archive.append(entry)
|
archive.append(entry)
|
||||||
all_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))
|
result = tag_template.render(dict(archive=archive, tag=tag))
|
||||||
with open(f'{output_dir}/tags/{tag}.html', 'w') as fh:
|
with open(f'{output_dir}/tags/{tag}.html', 'w') as fh:
|
||||||
fh.write(result)
|
fh.write(result)
|
||||||
|
|
||||||
|
|
||||||
def quickstart(args):
|
|
||||||
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 Does's Blog]? ")
|
|
||||||
author = 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)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
101
blag/devserver.py
Normal file
101
blag/devserver.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
"""Development Server.
|
||||||
|
|
||||||
|
This module provides functionality for blag's development server. It
|
||||||
|
automatically detects changes in certain directories and rebuilds the
|
||||||
|
site if necessary.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# remove when we don't support py38 anymore
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import NoReturn
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import multiprocessing
|
||||||
|
from http.server import SimpleHTTPRequestHandler, HTTPServer
|
||||||
|
from functools import partial
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from blag import blag
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_last_modified(dirs: list[str]) -> float:
|
||||||
|
"""Get the last modified time.
|
||||||
|
|
||||||
|
This method recursively goes through `dirs` and returns the most
|
||||||
|
recent modification time time found.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
dirs
|
||||||
|
list of directories to search
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
most recent modification time found in `dirs`
|
||||||
|
|
||||||
|
"""
|
||||||
|
last_mtime = 0.0
|
||||||
|
|
||||||
|
for dir in dirs:
|
||||||
|
for root, dirs, files in os.walk(dir):
|
||||||
|
for f in files:
|
||||||
|
mtime = os.stat(os.path.join(root, f)).st_mtime
|
||||||
|
if mtime > last_mtime:
|
||||||
|
last_mtime = mtime
|
||||||
|
|
||||||
|
return last_mtime
|
||||||
|
|
||||||
|
|
||||||
|
def autoreload(args: argparse.Namespace) -> NoReturn:
|
||||||
|
"""Start the autoreloader.
|
||||||
|
|
||||||
|
This method monitors the given directories for changes (i.e. the
|
||||||
|
last modified time). If the last modified time has changed, a
|
||||||
|
rebuild is triggered.
|
||||||
|
|
||||||
|
A rebuild is also performed immediately when this method is called
|
||||||
|
to avoid serving stale contents.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
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...')
|
||||||
|
# make sure we trigger the rebuild immediately when we enter the
|
||||||
|
# loop to avoid serving stale contents
|
||||||
|
last_mtime = 0.0
|
||||||
|
while True:
|
||||||
|
mtime = get_last_modified(dirs)
|
||||||
|
if mtime > last_mtime:
|
||||||
|
last_mtime = mtime
|
||||||
|
logger.info('Change detected, rebuilding...')
|
||||||
|
blag.build(args)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
def serve(args: argparse.Namespace) -> None:
|
||||||
|
"""Start the webserver and the autoreloader.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
args
|
||||||
|
contains the input-, template- and static 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")
|
||||||
|
httpd.serve_forever()
|
||||||
@@ -1,6 +1,16 @@
|
|||||||
|
"""Markdown Processing.
|
||||||
|
|
||||||
|
This module contains the methods responsible for blag's markdown
|
||||||
|
processing.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# remove when we don't support py38 anymore
|
||||||
|
from __future__ import annotations
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
from urllib.parse import urlsplit, urlunsplit
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
|
from xml.etree.ElementTree import Element
|
||||||
|
|
||||||
from markdown import Markdown
|
from markdown import Markdown
|
||||||
from markdown.extensions import Extension
|
from markdown.extensions import Extension
|
||||||
@@ -10,7 +20,7 @@ from markdown.treeprocessors import Treeprocessor
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def markdown_factory():
|
def markdown_factory() -> Markdown:
|
||||||
"""Create a Markdown instance.
|
"""Create a Markdown instance.
|
||||||
|
|
||||||
This method exists only to ensure we use the same Markdown instance
|
This method exists only to ensure we use the same Markdown instance
|
||||||
@@ -23,31 +33,44 @@ def markdown_factory():
|
|||||||
"""
|
"""
|
||||||
md = Markdown(
|
md = Markdown(
|
||||||
extensions=[
|
extensions=[
|
||||||
'meta', 'fenced_code', 'codehilite',
|
'meta',
|
||||||
MarkdownLinkExtension()
|
'fenced_code',
|
||||||
|
'codehilite',
|
||||||
|
'smarty',
|
||||||
|
MarkdownLinkExtension(),
|
||||||
],
|
],
|
||||||
output_format='html5',
|
output_format='html',
|
||||||
)
|
)
|
||||||
return md
|
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.
|
"""Convert markdown into html and extract meta data.
|
||||||
|
|
||||||
|
Some meta data is treated special:
|
||||||
|
* `date` is converted into datetime with local timezone
|
||||||
|
* `tags` is interpreted as a comma-separeted list of strings.
|
||||||
|
All strings are stripped and converted to lower case.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
md : markdown.Markdown instance
|
md
|
||||||
markdown : str
|
the Markdown instance
|
||||||
|
markdown
|
||||||
|
the markdown text that should be converted
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
str, dict :
|
str, dict[str, str]
|
||||||
html and metadata
|
html and metadata
|
||||||
|
|
||||||
"""
|
"""
|
||||||
md.reset()
|
md.reset()
|
||||||
content = md.convert(markdown)
|
content = md.convert(markdown)
|
||||||
meta = md.Meta
|
meta = md.Meta # type: ignore
|
||||||
|
|
||||||
# markdowns metadata consists as list of strings -- one item per
|
# markdowns metadata consists as list of strings -- one item per
|
||||||
# line. let's convert into single strings.
|
# line. let's convert into single strings.
|
||||||
@@ -71,24 +94,26 @@ def convert_markdown(md, markdown):
|
|||||||
|
|
||||||
|
|
||||||
class MarkdownLinkTreeprocessor(Treeprocessor):
|
class MarkdownLinkTreeprocessor(Treeprocessor):
|
||||||
"""Converts relative links to .md files to .html
|
"""Converts relative links to .md files to .html"""
|
||||||
|
|
||||||
"""
|
def run(self, root: Element) -> Element:
|
||||||
|
|
||||||
def run(self, root):
|
|
||||||
for element in root.iter():
|
for element in root.iter():
|
||||||
if element.tag == 'a':
|
if element.tag == 'a':
|
||||||
url = element.get('href')
|
url = element.get('href')
|
||||||
|
# element.get could also return None, we haven't seen this so
|
||||||
|
# far, so lets wait if we raise this
|
||||||
|
assert url is not None
|
||||||
|
url = str(url)
|
||||||
converted = self.convert(url)
|
converted = self.convert(url)
|
||||||
element.set('href', converted)
|
element.set('href', converted)
|
||||||
return root
|
return root
|
||||||
|
|
||||||
def convert(self, url):
|
def convert(self, url: str) -> str:
|
||||||
scheme, netloc, path, query, fragment = urlsplit(url)
|
scheme, netloc, path, query, fragment = urlsplit(url)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'{url}: {scheme=} {netloc=} {path=} {query=} {fragment=}'
|
f'{url}: {scheme=} {netloc=} {path=} {query=} {fragment=}'
|
||||||
)
|
)
|
||||||
if (scheme or netloc or not path):
|
if scheme or netloc or not path:
|
||||||
return url
|
return url
|
||||||
if path.endswith('.md'):
|
if path.endswith('.md'):
|
||||||
path = path[:-3] + '.html'
|
path = path[:-3] + '.html'
|
||||||
@@ -98,7 +123,11 @@ class MarkdownLinkTreeprocessor(Treeprocessor):
|
|||||||
|
|
||||||
|
|
||||||
class MarkdownLinkExtension(Extension):
|
class MarkdownLinkExtension(Extension):
|
||||||
def extendMarkdown(self, md):
|
"""markdown.extension that converts relative .md- to .html-links."""
|
||||||
|
|
||||||
|
def extendMarkdown(self, md: Markdown) -> None:
|
||||||
md.treeprocessors.register(
|
md.treeprocessors.register(
|
||||||
MarkdownLinkTreeprocessor(md), 'mdlink', 0,
|
MarkdownLinkTreeprocessor(md),
|
||||||
|
'mdlink',
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
|
|||||||
73
blag/quickstart.py
Normal file
73
blag/quickstart.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
"""Helper methods for blag's quickstart command.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# remove when we don't support py38 anymore
|
||||||
|
from __future__ import annotations
|
||||||
|
import configparser
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
|
||||||
|
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 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.
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -7,6 +7,11 @@
|
|||||||
|
|
||||||
{% if entry.title %}
|
{% if entry.title %}
|
||||||
<h1><a href="{{entry.dst}}">{{entry.title}}</a></h1>
|
<h1><a href="{{entry.dst}}">{{entry.title}}</a></h1>
|
||||||
|
|
||||||
|
{% if entry.description %}
|
||||||
|
<p>— {{ entry.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p>Written on {{ entry.date.date() }}.</p>
|
<p>Written on {{ entry.date.date() }}.</p>
|
||||||
|
|||||||
@@ -3,5 +3,26 @@
|
|||||||
{% block title %}{{ title }}{% endblock %}
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% 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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -7,16 +7,20 @@
|
|||||||
<meta name="author" content="{{ site.author }}">
|
<meta name="author" content="{{ site.author }}">
|
||||||
{%- if description %}
|
{%- if description %}
|
||||||
<meta name="description" content="{{ description }}">
|
<meta name="description" content="{{ description }}">
|
||||||
{% endif %}
|
{%- else %}
|
||||||
<title>{% block title %}{% endblock %}</title>
|
<meta name="description" content="{{ site.description }}">
|
||||||
|
{%- endif %}
|
||||||
|
<title>{% block title %}{% endblock %} | {{ site.description }}</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h1>A Blog</h1>
|
<h1><a href="/">{{ site.title }}</a></h1>
|
||||||
<nav>
|
<nav>
|
||||||
|
<h2>{{ site.description }}</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/">Blog</a></li>
|
<li><a href="/">Blog</a></li>
|
||||||
|
<li><a href="/tags/">Tags</a></li>
|
||||||
<li><a href="/atom.xml">Atom Feed</a></li>
|
<li><a href="/atom.xml">Atom Feed</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -26,6 +30,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
|
|
||||||
{% if entry.title %}
|
{% if entry.title %}
|
||||||
<h1><a href="/{{entry.dst}}">{{entry.title}}</a></h1>
|
<h1><a href="/{{entry.dst}}">{{entry.title}}</a></h1>
|
||||||
|
|
||||||
|
{% if entry.description %}
|
||||||
|
<p>— {{ entry.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p>Written on {{ entry.date.date() }}.</p>
|
<p>Written on {{ entry.date.date() }}.</p>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__VERSION__ = '0.0.2'
|
__VERSION__ = '1.5.0'
|
||||||
|
|||||||
1
debian/blag-doc.docs
vendored
Normal file
1
debian/blag-doc.docs
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
build/html/
|
||||||
1
debian/blag.install
vendored
Normal file
1
debian/blag.install
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
build/man/blag.1 /usr/share/man/man1
|
||||||
55
debian/changelog
vendored
Normal file
55
debian/changelog
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
-- Bastian Venthur <venthur@debian.org> Fri, 10 Jun 2022 07:26:19 +0200
|
||||||
|
|
||||||
|
blag (1.3.0) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Initial release. Closes: #1012584
|
||||||
|
|
||||||
|
-- Bastian Venthur <venthur@debian.org> Sun, 05 Jun 2022 15:20:48 +0200
|
||||||
61
debian/control
vendored
Normal file
61
debian/control
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
Source: blag
|
||||||
|
Section: python
|
||||||
|
Priority: optional
|
||||||
|
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,
|
||||||
|
python3-feedgenerator,
|
||||||
|
python3-jinja2,
|
||||||
|
python3-pygments,
|
||||||
|
python3-pytest,
|
||||||
|
python3-pytest-cov,
|
||||||
|
python3-sphinx,
|
||||||
|
#Testsuite: autopkgtest-pkg-python
|
||||||
|
Standards-Version: 4.6.0.1
|
||||||
|
Homepage: https://github.com/venthur/blag
|
||||||
|
Vcs-Browser: https://github.com/venthur/blag
|
||||||
|
Vcs-Git: https://github.com/venthur/blag.git
|
||||||
|
|
||||||
|
Package: blag
|
||||||
|
Architecture: all
|
||||||
|
Depends:
|
||||||
|
${python3:Depends},
|
||||||
|
${misc:Depends},
|
||||||
|
Suggests:
|
||||||
|
python-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
|
||||||
|
* 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
|
||||||
|
|
||||||
|
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
|
||||||
|
* 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
|
||||||
|
.
|
||||||
|
This is the common documentation package.
|
||||||
35
debian/copyright
vendored
Normal file
35
debian/copyright
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||||
|
Source: https://github.com/venthur/blag
|
||||||
|
Upstream-Name: blag
|
||||||
|
Upstream-Contact: Bastian Venthur venthur@debian.org
|
||||||
|
|
||||||
|
Files:
|
||||||
|
*
|
||||||
|
Copyright:
|
||||||
|
2022 Bastian Venthur venthur@debian.org
|
||||||
|
License: MIT
|
||||||
|
|
||||||
|
Files:
|
||||||
|
debian/*
|
||||||
|
Copyright:
|
||||||
|
2022 Bastian Venthur <venthur@debian.org>
|
||||||
|
License: MIT
|
||||||
|
|
||||||
|
License: MIT
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
.
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
.
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
25
debian/rules
vendored
Executable file
25
debian/rules
vendored
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/make -f
|
||||||
|
|
||||||
|
# See debhelper(7) (uncomment to enable).
|
||||||
|
# Output every command that modifies files on the build system.
|
||||||
|
#export DH_VERBOSE = 1
|
||||||
|
|
||||||
|
export PYBUILD_DESTDIR=debian/blag
|
||||||
|
export PYBUILD_TEST_ARGS=--no-cov
|
||||||
|
export PYBUILD_NAME=blag
|
||||||
|
|
||||||
|
%:
|
||||||
|
dh $@ --with python3,sphinxdoc --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
|
||||||
1
debian/source/format
vendored
Normal file
1
debian/source/format
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.0 (native)
|
||||||
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# 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)
|
||||||
12
docs/api.rst
Normal file
12
docs/api.rst
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
API
|
||||||
|
===
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:toctree: api
|
||||||
|
|
||||||
|
blag.__init__
|
||||||
|
blag.version
|
||||||
|
blag.blag
|
||||||
|
blag.markdown
|
||||||
|
blag.devserver
|
||||||
|
blag.quickstart
|
||||||
283
docs/blag.rst
Normal file
283
docs/blag.rst
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
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
|
||||||
|
|
||||||
69
docs/conf.py
Normal file
69
docs/conf.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# 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
|
||||||
53
docs/index.rst
Normal file
53
docs/index.rst
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
.. 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`
|
||||||
35
docs/make.bat
Normal file
35
docs/make.bat
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
@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
|
||||||
70
pyproject.toml
Normal file
70
pyproject.toml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
[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",
|
||||||
|
"sphinx",
|
||||||
|
"twine",
|
||||||
|
"wheel",
|
||||||
|
"pytest",
|
||||||
|
"pytest-cov",
|
||||||
|
"flake8",
|
||||||
|
"mypy",
|
||||||
|
"types-markdown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
version = {attr = "blag.__VERSION__" }
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
packages = [
|
||||||
|
"blag",
|
||||||
|
"blag.templates",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
blag = ["templates/*"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
addopts = """
|
||||||
|
--cov=blag
|
||||||
|
--cov=tests
|
||||||
|
--cov-report=html
|
||||||
|
--cov-report=term-missing:skip-covered
|
||||||
|
"""
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
files = "blag,tests"
|
||||||
|
strict = true
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "feedgenerator.*"
|
||||||
|
ignore_missing_imports = true
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
twine==3.3.0
|
build==0.9.0
|
||||||
wheel==0.36.2
|
sphinx==5.3.0
|
||||||
pytest==6.2.1
|
twine==4.0.2
|
||||||
pytest-cov==2.10.1
|
wheel==0.40.0
|
||||||
flake8==3.8.4
|
pytest==7.3.0
|
||||||
|
pytest-cov==4.0.0
|
||||||
|
flake8==6.0.0
|
||||||
|
mypy==1.2.0
|
||||||
|
types-markdown==3.4.2.1
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
markdown==3.3.3
|
markdown==3.4.1
|
||||||
feedgenerator==1.9.1
|
feedgenerator==2.0.0
|
||||||
jinja2==2.11.2
|
jinja2==3.1.2
|
||||||
pygments==2.7.3
|
pygments==2.13.0
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
[tool:pytest]
|
|
||||||
addopts =
|
|
||||||
--cov=blag
|
|
||||||
--cov=tests
|
|
||||||
--cov-report=html
|
|
||||||
--cov-report=term-missing:skip-covered
|
|
||||||
|
|
||||||
[flake8]
|
|
||||||
exclude = venv,build
|
|
||||||
37
setup.py
37
setup.py
@@ -1,37 +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='simple 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',
|
|
||||||
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',
|
|
||||||
)
|
|
||||||
84
tests/conftest.py
Normal file
84
tests/conftest.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# remove when we don't support py38 anymore
|
||||||
|
from __future__ import annotations
|
||||||
|
from argparse import Namespace
|
||||||
|
from typing import Iterator, Callable
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from jinja2 import Environment, Template
|
||||||
|
|
||||||
|
from blag import blag
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def environment() -> Iterator[Environment]:
|
||||||
|
site = {
|
||||||
|
'base_url': 'site base_url',
|
||||||
|
'title': 'site title',
|
||||||
|
'description': 'site description',
|
||||||
|
'author': 'site author',
|
||||||
|
}
|
||||||
|
env = blag.environment_factory(globals_=dict(site=site))
|
||||||
|
yield env
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def page_template(environment: Environment) -> Iterator[Template]:
|
||||||
|
yield environment.get_template('page.html')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def article_template(environment: Environment) -> Iterator[Template]:
|
||||||
|
yield environment.get_template('article.html')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def archive_template(environment: Environment) -> Iterator[Template]:
|
||||||
|
yield environment.get_template('archive.html')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def tags_template(environment: Environment) -> Iterator[Template]:
|
||||||
|
yield environment.get_template('tags.html')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def tag_template(environment: Environment) -> Iterator[Template]:
|
||||||
|
yield environment.get_template('tag.html')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def cleandir() -> Iterator[str]:
|
||||||
|
"""Create a temporary workind directory and cwd."""
|
||||||
|
config = """
|
||||||
|
[main]
|
||||||
|
base_url = https://example.com/
|
||||||
|
title = title
|
||||||
|
description = description
|
||||||
|
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:
|
||||||
|
fh.write(config)
|
||||||
|
# change directory
|
||||||
|
old_cwd = os.getcwd()
|
||||||
|
os.chdir(dir)
|
||||||
|
yield dir
|
||||||
|
# and change back afterwards
|
||||||
|
os.chdir(old_cwd)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def args(cleandir: Callable[[], Iterator[str]]) -> Iterator[Namespace]:
|
||||||
|
|
||||||
|
args = Namespace(
|
||||||
|
input_dir='content',
|
||||||
|
output_dir='build',
|
||||||
|
static_dir='static',
|
||||||
|
template_dir='templates',
|
||||||
|
)
|
||||||
|
yield args
|
||||||
@@ -1,24 +1,103 @@
|
|||||||
|
# remove when we don't support py38 anymore
|
||||||
|
from __future__ import annotations
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
import os
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
from argparse import Namespace
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from pytest import CaptureFixture, LogCaptureFixture
|
||||||
|
from jinja2 import Template
|
||||||
|
|
||||||
|
from blag import __VERSION__
|
||||||
from blag import blag
|
from blag import blag
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
def test_generate_feed(cleandir: str) -> None:
|
||||||
def outdir():
|
articles: list[tuple[str, dict[str, Any]]] = []
|
||||||
with TemporaryDirectory() as dir:
|
blag.generate_feed(articles, 'build', ' ', ' ', ' ', ' ')
|
||||||
yield dir
|
assert os.path.exists('build/atom.xml')
|
||||||
|
|
||||||
|
|
||||||
def test_generate_feed(outdir):
|
def test_feed(cleandir: str) -> None:
|
||||||
articles = []
|
articles: list[tuple[str, dict[str, Any]]] = [
|
||||||
blag.generate_feed(articles, outdir, ' ', ' ', ' ', ' ')
|
(
|
||||||
assert os.path.exists(f'{outdir}/atom.xml')
|
'dest1.html',
|
||||||
|
{
|
||||||
|
'title': 'title1',
|
||||||
|
'date': datetime(2019, 6, 6),
|
||||||
|
'content': 'content1',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'dest2.html',
|
||||||
|
{
|
||||||
|
'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:
|
||||||
|
feed = fh.read()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# article 1
|
||||||
|
assert '<title>title1</title>' in feed
|
||||||
|
assert '<summary type="html">title1' 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 '<summary type="html">title2' 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_parse_args_build():
|
def test_generate_feed_with_description(cleandir: str) -> None:
|
||||||
|
# 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: 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:
|
||||||
|
feed = fh.read()
|
||||||
|
|
||||||
|
assert '<title>title</title>' in feed
|
||||||
|
assert '<summary type="html">description' in feed
|
||||||
|
assert '<published>2019-06-06' in feed
|
||||||
|
assert '<content type="html">content' in feed
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_build() -> None:
|
||||||
# test default args
|
# test default args
|
||||||
args = blag.parse_args(['build'])
|
args = blag.parse_args(['build'])
|
||||||
assert args.input_dir == 'content'
|
assert args.input_dir == 'content'
|
||||||
@@ -51,7 +130,7 @@ def test_parse_args_build():
|
|||||||
assert args.static_dir == 'foo'
|
assert args.static_dir == 'foo'
|
||||||
|
|
||||||
|
|
||||||
def test_get_config():
|
def test_get_config() -> None:
|
||||||
config = """
|
config = """
|
||||||
[main]
|
[main]
|
||||||
base_url = https://example.com/
|
base_url = https://example.com/
|
||||||
@@ -73,10 +152,9 @@ author = a. u. thor
|
|||||||
|
|
||||||
# a missing required config causes a sys.exit
|
# a missing required config causes a sys.exit
|
||||||
for x in 'base_url', 'title', 'description', 'author':
|
for x in 'base_url', 'title', 'description', 'author':
|
||||||
config2 = '\n'.join([line
|
config2 = '\n'.join(
|
||||||
for line
|
[line for line in config.splitlines() if not line.startswith(x)]
|
||||||
in config.splitlines()
|
)
|
||||||
if not line.startswith(x)])
|
|
||||||
with TemporaryDirectory() as dir:
|
with TemporaryDirectory() as dir:
|
||||||
configfile = f'{dir}/config.ini'
|
configfile = f'{dir}/config.ini'
|
||||||
with open(configfile, 'w') as fh:
|
with open(configfile, 'w') as fh:
|
||||||
@@ -101,11 +179,143 @@ author = a. u. thor
|
|||||||
assert config_parsed['base_url'] == 'https://example.com/'
|
assert config_parsed['base_url'] == 'https://example.com/'
|
||||||
|
|
||||||
|
|
||||||
def test_environment_factory():
|
def test_environment_factory() -> None:
|
||||||
globals_ = {
|
globals_: dict[str, object] = {'foo': 'bar', 'test': 'me'}
|
||||||
'foo': 'bar',
|
|
||||||
'test': 'me'
|
|
||||||
}
|
|
||||||
env = blag.environment_factory(globals_=globals_)
|
env = blag.environment_factory(globals_=globals_)
|
||||||
assert env.globals['foo'] == 'bar'
|
assert env.globals['foo'] == 'bar'
|
||||||
assert env.globals['test'] == 'me'
|
assert env.globals['test'] == 'me'
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_markdown(
|
||||||
|
cleandir: str,
|
||||||
|
page_template: Template,
|
||||||
|
article_template: Template,
|
||||||
|
) -> None:
|
||||||
|
page1 = """\
|
||||||
|
title: some page
|
||||||
|
|
||||||
|
some text
|
||||||
|
foo bar
|
||||||
|
"""
|
||||||
|
|
||||||
|
article1 = """\
|
||||||
|
title: some article1
|
||||||
|
date: 2020-01-01
|
||||||
|
|
||||||
|
some text
|
||||||
|
foo bar
|
||||||
|
"""
|
||||||
|
|
||||||
|
article2 = """\
|
||||||
|
title: some article2
|
||||||
|
date: 2021-01-01
|
||||||
|
|
||||||
|
some text
|
||||||
|
foo bar
|
||||||
|
"""
|
||||||
|
|
||||||
|
convertibles = []
|
||||||
|
for i, txt in enumerate((page1, article1, article2)):
|
||||||
|
with open(f'content/{str(i)}', 'w') as fh:
|
||||||
|
fh.write(txt)
|
||||||
|
convertibles.append((str(i), str(i)))
|
||||||
|
|
||||||
|
articles, pages = blag.process_markdown(
|
||||||
|
convertibles, 'content', 'build', page_template, article_template
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(articles, list)
|
||||||
|
assert len(articles) == 2
|
||||||
|
for dst, context in articles:
|
||||||
|
assert isinstance(dst, str)
|
||||||
|
assert isinstance(context, dict)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def test_build(args: Namespace) -> None:
|
||||||
|
page1 = """\
|
||||||
|
title: some page
|
||||||
|
|
||||||
|
some text
|
||||||
|
foo bar
|
||||||
|
"""
|
||||||
|
|
||||||
|
article1 = """\
|
||||||
|
title: some article1
|
||||||
|
date: 2020-01-01
|
||||||
|
tags: foo, bar
|
||||||
|
|
||||||
|
some text
|
||||||
|
foo bar
|
||||||
|
"""
|
||||||
|
|
||||||
|
article2 = """\
|
||||||
|
title: some article2
|
||||||
|
date: 2021-01-01
|
||||||
|
tags: baz
|
||||||
|
|
||||||
|
some text
|
||||||
|
foo bar
|
||||||
|
"""
|
||||||
|
|
||||||
|
# write some convertibles
|
||||||
|
convertibles = []
|
||||||
|
for i, txt in enumerate((page1, article1, article2)):
|
||||||
|
with open(f'{args.input_dir}/{str(i)}.md', 'w') as fh:
|
||||||
|
fh.write(txt)
|
||||||
|
convertibles.append((str(i), str(i)))
|
||||||
|
|
||||||
|
# some static files
|
||||||
|
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')
|
||||||
|
|
||||||
|
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')
|
||||||
|
# ... archive
|
||||||
|
assert os.path.exists(f'{args.output_dir}/index.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: str) -> None:
|
||||||
|
blag.main(['build'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_version(capsys: CaptureFixture[str]) -> None:
|
||||||
|
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:
|
||||||
|
blag.main(['build'])
|
||||||
|
assert 'DEBUG' not in caplog.text
|
||||||
|
|
||||||
|
blag.main(['--verbose', 'build'])
|
||||||
|
assert 'DEBUG' in caplog.text
|
||||||
|
|||||||
75
tests/test_devserver.py
Normal file
75
tests/test_devserver.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# remove when we don't support py38 anymore
|
||||||
|
from __future__ import annotations
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
from argparse import Namespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from blag import devserver
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_last_modified(cleandir: str) -> None:
|
||||||
|
# take initial time
|
||||||
|
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'])
|
||||||
|
|
||||||
|
# wait a bit and take time again
|
||||||
|
time.sleep(0.1)
|
||||||
|
t3 = devserver.get_last_modified(['content'])
|
||||||
|
|
||||||
|
assert t2 > t1
|
||||||
|
assert t2 == t3
|
||||||
|
|
||||||
|
|
||||||
|
def test_autoreload_builds_immediately(args: Namespace) -> None:
|
||||||
|
# create a dummy file that can be build
|
||||||
|
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.start()
|
||||||
|
# try for 5 seconds...
|
||||||
|
for i in range(5):
|
||||||
|
time.sleep(1)
|
||||||
|
t1 = devserver.get_last_modified(['build'])
|
||||||
|
print(t1)
|
||||||
|
if t1 > t0:
|
||||||
|
break
|
||||||
|
assert t1 > t0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.filterwarnings(
|
||||||
|
"ignore::pytest.PytestUnhandledThreadExceptionWarning"
|
||||||
|
)
|
||||||
|
def test_autoreload(args: Namespace) -> None:
|
||||||
|
t = threading.Thread(
|
||||||
|
target=devserver.autoreload,
|
||||||
|
args=(args,),
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
# try for 5 seconds to see if we rebuild once...
|
||||||
|
for i in range(5):
|
||||||
|
time.sleep(1)
|
||||||
|
t1 = devserver.get_last_modified(['build'])
|
||||||
|
if t1 > t0:
|
||||||
|
break
|
||||||
|
assert t1 > t0
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
|
# remove when we don't support py38 anymore
|
||||||
|
from __future__ import annotations
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import markdown
|
import markdown
|
||||||
@@ -6,46 +9,99 @@ import markdown
|
|||||||
from blag.markdown import convert_markdown, markdown_factory
|
from blag.markdown import convert_markdown, markdown_factory
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("input_, expected", [
|
@pytest.mark.parametrize(
|
||||||
# inline
|
"input_, expected",
|
||||||
('[test](test.md)', 'test.html'),
|
[
|
||||||
('[test](test.md "test")', 'test.html'),
|
# inline
|
||||||
('[test](a/test.md)', 'a/test.html'),
|
('[test](test.md)', 'test.html'),
|
||||||
('[test](a/test.md "test")', 'a/test.html'),
|
('[test](test.md "test")', 'test.html'),
|
||||||
('[test](/test.md)', '/test.html'),
|
('[test](a/test.md)', 'a/test.html'),
|
||||||
('[test](/test.md "test")', '/test.html'),
|
('[test](a/test.md "test")', 'a/test.html'),
|
||||||
('[test](/a/test.md)', '/a/test.html'),
|
('[test](/test.md)', '/test.html'),
|
||||||
('[test](/a/test.md "test")', '/a/test.html'),
|
('[test](/test.md "test")', '/test.html'),
|
||||||
# reference
|
('[test](/a/test.md)', '/a/test.html'),
|
||||||
('[test][]\n[test]: test.md ''', 'test.html'),
|
('[test](/a/test.md "test")', '/a/test.html'),
|
||||||
('[test][]\n[test]: test.md "test"', 'test.html'),
|
# reference
|
||||||
('[test][]\n[test]: a/test.md', 'a/test.html'),
|
('[test][]\n[test]: test.md ' '', 'test.html'),
|
||||||
('[test][]\n[test]: a/test.md "test"', 'a/test.html'),
|
('[test][]\n[test]: test.md "test"', 'test.html'),
|
||||||
('[test][]\n[test]: /test.md', '/test.html'),
|
('[test][]\n[test]: a/test.md', 'a/test.html'),
|
||||||
('[test][]\n[test]: /test.md "test"', '/test.html'),
|
('[test][]\n[test]: a/test.md "test"', 'a/test.html'),
|
||||||
('[test][]\n[test]: /a/test.md', '/a/test.html'),
|
('[test][]\n[test]: /test.md', '/test.html'),
|
||||||
('[test][]\n[test]: /a/test.md "test"', '/a/test.html'),
|
('[test][]\n[test]: /test.md "test"', '/test.html'),
|
||||||
])
|
('[test][]\n[test]: /a/test.md', '/a/test.html'),
|
||||||
def test_convert_markdown_links(input_, expected):
|
('[test][]\n[test]: /a/test.md "test"', '/a/test.html'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_convert_markdown_links(input_: str, expected: str) -> None:
|
||||||
md = markdown_factory()
|
md = markdown_factory()
|
||||||
html, _ = convert_markdown(md, input_)
|
html, _ = convert_markdown(md, input_)
|
||||||
assert expected in html
|
assert expected in html
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("input_, expected", [
|
@pytest.mark.parametrize(
|
||||||
('foo: bar', {'foo': 'bar'}),
|
"input_, expected",
|
||||||
('foo: those are several words', {'foo': 'those are several words'}),
|
[
|
||||||
('tags: this, is, a, test\n', {'tags': ['this', 'is', 'a', 'test']}),
|
# scheme
|
||||||
('tags: this, IS, a, test', {'tags': ['this', 'is', 'a', 'test']}),
|
('[test](https://)', 'https://'),
|
||||||
('date: 2020-01-01 12:10', {'date':
|
# netloc
|
||||||
datetime(2020, 1, 1, 12, 10).astimezone()}),
|
('[test](//test.md)', '//test.md'),
|
||||||
])
|
# no path
|
||||||
def test_convert_metadata(input_, expected):
|
('[test]()', ''),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_dont_convert_normal_links(input_: str, expected: str) -> None:
|
||||||
|
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:
|
||||||
md = markdown_factory()
|
md = markdown_factory()
|
||||||
_, meta = convert_markdown(md, input_)
|
_, meta = convert_markdown(md, input_)
|
||||||
assert expected == meta
|
assert expected == meta
|
||||||
|
|
||||||
|
|
||||||
def test_markdown_factory():
|
def test_markdown_factory() -> None:
|
||||||
md = markdown_factory()
|
md = markdown_factory()
|
||||||
assert isinstance(md, markdown.Markdown)
|
assert isinstance(md, markdown.Markdown)
|
||||||
|
|
||||||
|
|
||||||
|
def test_smarty() -> None:
|
||||||
|
md = markdown_factory()
|
||||||
|
|
||||||
|
md1 = """
|
||||||
|
|
||||||
|
this --- is -- a test ...
|
||||||
|
|
||||||
|
"""
|
||||||
|
html, meta = convert_markdown(md, md1)
|
||||||
|
assert 'mdash' in html
|
||||||
|
assert 'ndash' in html
|
||||||
|
assert 'hellip' in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_smarty_code() -> None:
|
||||||
|
md = markdown_factory()
|
||||||
|
|
||||||
|
md1 = """
|
||||||
|
```
|
||||||
|
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
|
||||||
|
|||||||
29
tests/test_quickstart.py
Normal file
29
tests/test_quickstart.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# remove when we don't support py38 anymore
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pytest import MonkeyPatch
|
||||||
|
|
||||||
|
from blag.quickstart import get_input, quickstart
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_input_default_answer(monkeypatch: MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr('builtins.input', lambda x: '')
|
||||||
|
answer = get_input("foo", "bar")
|
||||||
|
assert answer == 'bar'
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_input(monkeypatch: MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr('builtins.input', lambda x: 'baz')
|
||||||
|
answer = get_input("foo", "bar")
|
||||||
|
assert answer == 'baz'
|
||||||
|
|
||||||
|
|
||||||
|
def test_quickstart(cleandir: str, monkeypatch: MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr('builtins.input', lambda x: 'foo')
|
||||||
|
quickstart(None)
|
||||||
|
with open('config.ini', 'r') 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
|
||||||
@@ -1,48 +1,11 @@
|
|||||||
|
# remove when we don't support py38 anymore
|
||||||
|
from __future__ import annotations
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import pytest
|
from jinja2 import Template
|
||||||
|
|
||||||
from blag import blag
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
def test_page(page_template: Template) -> None:
|
||||||
def environment():
|
|
||||||
site = {
|
|
||||||
'base_url': 'site base_url',
|
|
||||||
'title': 'site title',
|
|
||||||
'description': 'site description',
|
|
||||||
'author': 'site author',
|
|
||||||
}
|
|
||||||
env = blag.environment_factory(globals_=dict(site=site))
|
|
||||||
yield env
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def page_template(environment):
|
|
||||||
yield environment.get_template('page.html')
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def article_template(environment):
|
|
||||||
yield environment.get_template('article.html')
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def archive_template(environment):
|
|
||||||
yield environment.get_template('archive.html')
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def tags_template(environment):
|
|
||||||
yield environment.get_template('tags.html')
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def tag_template(environment):
|
|
||||||
yield environment.get_template('tag.html')
|
|
||||||
|
|
||||||
|
|
||||||
def test_page(page_template):
|
|
||||||
ctx = {
|
ctx = {
|
||||||
'content': 'this is the content',
|
'content': 'this is the content',
|
||||||
'title': 'this is the title',
|
'title': 'this is the title',
|
||||||
@@ -52,17 +15,19 @@ def test_page(page_template):
|
|||||||
assert 'this is the title' in result
|
assert 'this is the title' in result
|
||||||
|
|
||||||
|
|
||||||
def test_article(article_template):
|
def test_article(article_template: Template) -> None:
|
||||||
ctx = {
|
ctx = {
|
||||||
'content': 'this is the content',
|
'content': 'this is the content',
|
||||||
'title': 'this is the title',
|
'title': 'this is the title',
|
||||||
|
'date': datetime.datetime(1980, 5, 9),
|
||||||
}
|
}
|
||||||
result = article_template.render(ctx)
|
result = article_template.render(ctx)
|
||||||
assert 'this is the content' in result
|
assert 'this is the content' in result
|
||||||
assert 'this is the title' in result
|
assert 'this is the title' in result
|
||||||
|
assert '1980-05-09' in result
|
||||||
|
|
||||||
|
|
||||||
def test_archive(archive_template):
|
def test_archive(archive_template: Template) -> None:
|
||||||
entry = {
|
entry = {
|
||||||
'title': 'this is a title',
|
'title': 'this is a title',
|
||||||
'dst': 'https://example.com/link',
|
'dst': 'https://example.com/link',
|
||||||
@@ -80,7 +45,7 @@ def test_archive(archive_template):
|
|||||||
assert 'https://example.com/link' in result
|
assert 'https://example.com/link' in result
|
||||||
|
|
||||||
|
|
||||||
def test_tags(tags_template):
|
def test_tags(tags_template: Template) -> None:
|
||||||
tags = [('foo', 42)]
|
tags = [('foo', 42)]
|
||||||
ctx = {
|
ctx = {
|
||||||
'tags': tags,
|
'tags': tags,
|
||||||
@@ -93,7 +58,7 @@ def test_tags(tags_template):
|
|||||||
assert '42' in result
|
assert '42' in result
|
||||||
|
|
||||||
|
|
||||||
def test_tag(tag_template):
|
def test_tag(tag_template: Template) -> None:
|
||||||
entry = {
|
entry = {
|
||||||
'title': 'this is a title',
|
'title': 'this is a title',
|
||||||
'dst': 'https://example.com/link',
|
'dst': 'https://example.com/link',
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
# remove when we don't support py38 anymore
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import blag
|
import blag
|
||||||
|
|
||||||
|
|
||||||
def test_version():
|
def test_version() -> None:
|
||||||
assert isinstance(blag.__VERSION__, str)
|
assert isinstance(blag.__VERSION__, str)
|
||||||
|
|||||||
Reference in New Issue
Block a user