mirror of
https://github.com/venthur/blag.git
synced 2025-11-25 20:52:43 +00:00
WIP
This commit is contained in:
4
Makefile
4
Makefile
@@ -27,6 +27,10 @@ $(VENV): requirements.txt requirements-dev.txt setup.py
|
|||||||
test: $(VENV)
|
test: $(VENV)
|
||||||
$(BIN)/pytest
|
$(BIN)/pytest
|
||||||
|
|
||||||
|
.PHONY: mypy
|
||||||
|
mypy: $(VENV)
|
||||||
|
$(BIN)/mypy
|
||||||
|
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
lint: $(VENV)
|
lint: $(VENV)
|
||||||
$(BIN)/flake8
|
$(BIN)/flake8
|
||||||
|
|||||||
83
blag/blag.py
83
blag/blag.py
@@ -11,7 +11,13 @@ 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
|
||||||
@@ -26,7 +32,7 @@ logging.basicConfig(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def main(args=None):
|
def main(arguments: list[str] = None) -> None:
|
||||||
"""Main entrypoint for the CLI.
|
"""Main entrypoint for the CLI.
|
||||||
|
|
||||||
This method parses the CLI arguments and executes the respective
|
This method parses the CLI arguments and executes the respective
|
||||||
@@ -34,11 +40,11 @@ def main(args=None):
|
|||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
args : list[str]
|
arguments : list[str]
|
||||||
optional parameters, used for testing
|
optional parameters, used for testing
|
||||||
|
|
||||||
"""
|
"""
|
||||||
args = parse_args(args)
|
args = parse_args(arguments)
|
||||||
# set loglevel
|
# set loglevel
|
||||||
if args.verbose:
|
if args.verbose:
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
@@ -46,7 +52,7 @@ def main(args=None):
|
|||||||
args.func(args)
|
args.func(args)
|
||||||
|
|
||||||
|
|
||||||
def parse_args(args=None):
|
def parse_args(args: list[str] = None) -> argparse.Namespace:
|
||||||
"""Parse command line arguments.
|
"""Parse command line arguments.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -135,7 +141,7 @@ def parse_args(args=None):
|
|||||||
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.
|
"""Load site configuration from configfile.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -146,7 +152,7 @@ def get_config(configfile):
|
|||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
dict
|
configparser.SectionProxy
|
||||||
|
|
||||||
"""
|
"""
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
@@ -166,7 +172,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,
|
||||||
|
globals_: dict = None,
|
||||||
|
) -> Environment:
|
||||||
"""Environment factory.
|
"""Environment factory.
|
||||||
|
|
||||||
Creates a Jinja2 Environment with the default templates and
|
Creates a Jinja2 Environment with the default templates and
|
||||||
@@ -186,7 +195,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'))
|
||||||
@@ -196,7 +205,7 @@ def environment_factory(template_dir=None, globals_=None):
|
|||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
||||||
def build(args):
|
def build(args: argparse.Namespace) -> None:
|
||||||
"""Build the site.
|
"""Build the site.
|
||||||
|
|
||||||
This is blag's main method that builds the site, generates the feed
|
This is blag's main method that builds the site, generates the feed
|
||||||
@@ -261,8 +270,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]], list[tuple[str, dict]]]:
|
||||||
"""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
|
||||||
@@ -273,7 +287,7 @@ def process_markdown(convertibles, input_dir, output_dir,
|
|||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
convertibles : List[Tuple[str, str]]
|
convertibles : list[tuple[str, str]]
|
||||||
relative paths to markdown- (src) html- (dest) files
|
relative paths to markdown- (src) html- (dest) files
|
||||||
input_dir : str
|
input_dir : str
|
||||||
output_dir : str
|
output_dir : str
|
||||||
@@ -282,7 +296,7 @@ def process_markdown(convertibles, input_dir, output_dir,
|
|||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
articles, pages : List[Tuple[str, Dict]]
|
articles, pages : list[tuple[str, dict]]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
logger.info("Converting Markdown files...")
|
logger.info("Converting Markdown files...")
|
||||||
@@ -318,18 +332,18 @@ def process_markdown(convertibles, input_dir, output_dir,
|
|||||||
|
|
||||||
|
|
||||||
def generate_feed(
|
def generate_feed(
|
||||||
articles,
|
articles: list[tuple[str, dict]],
|
||||||
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.
|
"""Generate Atom feed.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
articles : list[list[str, dict]]
|
articles : list[tuple[str, dict]]
|
||||||
list of relative output path and article dictionary
|
list of relative output path and article dictionary
|
||||||
output_dir : str
|
output_dir : str
|
||||||
where the feed is stored
|
where the feed is stored
|
||||||
@@ -369,12 +383,16 @@ 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]],
|
||||||
|
template: Template,
|
||||||
|
output_dir: str,
|
||||||
|
) -> None:
|
||||||
"""Generate the archive page.
|
"""Generate the archive page.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
articles : list[list[str, dict]]
|
articles : list[tuple[str, dict]]
|
||||||
List of articles. Each article has the destination path and a
|
List of articles. Each article has the destination path and a
|
||||||
dictionary with the content.
|
dictionary with the content.
|
||||||
template : jinja2.Template instance
|
template : jinja2.Template instance
|
||||||
@@ -392,12 +410,17 @@ 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(
|
||||||
|
articles: list[tuple[str, dict]],
|
||||||
|
tags_template: Template,
|
||||||
|
tag_template: Template,
|
||||||
|
output_dir: str,
|
||||||
|
) -> None:
|
||||||
"""Generate the tags page.
|
"""Generate the tags page.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
articles : list[list[str, dict]]
|
articles : list[tuple[str, dict]]
|
||||||
List of articles. Each article has the destination path and a
|
List of articles. Each article has the destination path and a
|
||||||
dictionary with the content.
|
dictionary with the content.
|
||||||
tags_template, tag_template : jinja2.Template instance
|
tags_template, tag_template : jinja2.Template instance
|
||||||
@@ -408,15 +431,17 @@ def generate_tags(articles, tags_template, tag_template, output_dir):
|
|||||||
os.makedirs(f'{output_dir}/tags', exist_ok=True)
|
os.makedirs(f'{output_dir}/tags', exist_ok=True)
|
||||||
|
|
||||||
# get tags number of occurrences
|
# get tags number of occurrences
|
||||||
all_tags = {}
|
all_tags: dict = {}
|
||||||
for _, context in articles:
|
for _, context in articles:
|
||||||
tags = context.get('tags', [])
|
tags = 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)
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import time
|
|||||||
import multiprocessing
|
import multiprocessing
|
||||||
from http.server import SimpleHTTPRequestHandler, HTTPServer
|
from http.server import SimpleHTTPRequestHandler, HTTPServer
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
import argparse
|
||||||
|
|
||||||
from blag import blag
|
from blag import blag
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ from blag import blag
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_last_modified(dirs):
|
def get_last_modified(dirs: list[str]) -> float:
|
||||||
"""Get the last modified time.
|
"""Get the last modified time.
|
||||||
|
|
||||||
This method recursively goes through `dirs` and returns the most
|
This method recursively goes through `dirs` and returns the most
|
||||||
@@ -32,11 +33,11 @@ def get_last_modified(dirs):
|
|||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
int
|
float
|
||||||
most recent modification time found in `dirs`
|
most recent modification time found in `dirs`
|
||||||
|
|
||||||
"""
|
"""
|
||||||
last_mtime = 0
|
last_mtime = 0.0
|
||||||
|
|
||||||
for dir in dirs:
|
for dir in dirs:
|
||||||
for root, dirs, files in os.walk(dir):
|
for root, dirs, files in os.walk(dir):
|
||||||
@@ -48,7 +49,7 @@ def get_last_modified(dirs):
|
|||||||
return last_mtime
|
return last_mtime
|
||||||
|
|
||||||
|
|
||||||
def autoreload(args):
|
def autoreload(args: argparse.Namespace) -> None:
|
||||||
"""Start the autoreloader.
|
"""Start the autoreloader.
|
||||||
|
|
||||||
This method monitors the given directories for changes (i.e. the
|
This method monitors the given directories for changes (i.e. the
|
||||||
@@ -67,7 +68,7 @@ def autoreload(args):
|
|||||||
logger.info(f'Monitoring {dirs} for changes...')
|
logger.info(f'Monitoring {dirs} for changes...')
|
||||||
# make sure we trigger the rebuild immediately when we enter the
|
# make sure we trigger the rebuild immediately when we enter the
|
||||||
# loop to avoid serving stale contents
|
# loop to avoid serving stale contents
|
||||||
last_mtime = 0
|
last_mtime = 0.0
|
||||||
while True:
|
while True:
|
||||||
mtime = get_last_modified(dirs)
|
mtime = get_last_modified(dirs)
|
||||||
if mtime > last_mtime:
|
if mtime > last_mtime:
|
||||||
@@ -77,7 +78,7 @@ def autoreload(args):
|
|||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
def serve(args):
|
def serve(args: argparse.Namespace) -> None:
|
||||||
"""Start the webserver and the autoreloader.
|
"""Start the webserver and the autoreloader.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ processing.
|
|||||||
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
|
||||||
@@ -17,7 +18,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
|
||||||
@@ -33,12 +34,12 @@ def markdown_factory():
|
|||||||
'meta', 'fenced_code', 'codehilite', 'smarty',
|
'meta', 'fenced_code', 'codehilite', 'smarty',
|
||||||
MarkdownLinkExtension()
|
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]:
|
||||||
"""Convert markdown into html and extract meta data.
|
"""Convert markdown into html and extract meta data.
|
||||||
|
|
||||||
Some meta data is treated special:
|
Some meta data is treated special:
|
||||||
@@ -87,15 +88,19 @@ class MarkdownLinkTreeprocessor(Treeprocessor):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def run(self, root):
|
def run(self, root: Element):
|
||||||
for element in root.iter():
|
for element in root.iter():
|
||||||
if element.tag == 'a':
|
if element.tag == 'a':
|
||||||
url = element.get('href')
|
url = element.get('href')
|
||||||
|
# element.get could also return None, we haven't seen this so
|
||||||
|
# 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):
|
||||||
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=}'
|
||||||
@@ -113,7 +118,7 @@ class MarkdownLinkExtension(Extension):
|
|||||||
"""markdown.extension that converts relative .md- to .html-links.
|
"""markdown.extension that converts relative .md- to .html-links.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def extendMarkdown(self, md):
|
def extendMarkdown(self, md: Markdown):
|
||||||
md.treeprocessors.register(
|
md.treeprocessors.register(
|
||||||
MarkdownLinkTreeprocessor(md), 'mdlink', 0,
|
MarkdownLinkTreeprocessor(md), 'mdlink', 0,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import configparser
|
import configparser
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
|
||||||
def get_input(question, default):
|
def get_input(question: str, default: str) -> str:
|
||||||
"""Prompt for user input.
|
"""Prompt for user input.
|
||||||
|
|
||||||
This is a wrapper around the input-builtin. It will show the default answer
|
This is a wrapper around the input-builtin. It will show the default answer
|
||||||
@@ -29,7 +30,7 @@ def get_input(question, default):
|
|||||||
return reply
|
return reply
|
||||||
|
|
||||||
|
|
||||||
def quickstart(args):
|
def quickstart(args: argparse.Namespace) -> None:
|
||||||
"""Quickstart.
|
"""Quickstart.
|
||||||
|
|
||||||
This method asks the user some questions and generates a
|
This method asks the user some questions and generates a
|
||||||
|
|||||||
@@ -4,3 +4,5 @@ wheel==0.37.1
|
|||||||
pytest==7.1.2
|
pytest==7.1.2
|
||||||
pytest-cov==3.0.0
|
pytest-cov==3.0.0
|
||||||
flake8==5.0.2
|
flake8==5.0.2
|
||||||
|
mypy==0.971
|
||||||
|
types-markdown==3.4.1
|
||||||
|
|||||||
Reference in New Issue
Block a user