diff --git a/CHANGELOG.md b/CHANGELOG.md index eb9cf8d..be2f7eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,40 @@ New users are not affected as `blag quickstart` will generate the needed templates. +* Split former archive page which served as index.html into "index" and + "archive", each with their own template, respectively. Index is the landing + page and shows by default only the latest 10 articles. Archive shows the full + list of articles. + + If you used custom templates, + * you should create an "index.html"-template (take blag's default one as a + starting point) + * you may want to include the new "/archive.html" link somewhere in your + navigation + +### Changed + +* blag comes now with a simple yet good looking default theme that supports + syntax highlighting and a light- and dark theme. + +* apart from the generated configuration, `blag quickstart` will now also + create the initial directory structure, with the default template, the static + directory with the CSS files and the content directory with some initial + content to get the user started + +* Added a make target to update the pygments themes + +* updated dependencies: + * markdown 3.4.3 + * pygments 2.15.1 + * pytest 7.3.2 + * types-markdown 3.4.2.9 + * build 0.10.0 + +### Fixed + +* fixed pyproject.toml to include tests/conftest.py + ## [1.5.0] - 2023-04-16 diff --git a/Makefile b/Makefile index 5731772..fce9102 100644 --- a/Makefile +++ b/Makefile @@ -48,6 +48,11 @@ test-release: $(VENV) build release: $(VENV) build $(BIN)/twine upload dist/* +.PHONY: update-pygmentize +update-pygmentize: $(VENV) + $(BIN)/pygmentize -f html -S default > blag/static/code-light.css + $(BIN)/pygmentize -f html -S monokai > blag/static/code-dark.css + .PHONY: docs docs: $(VENV) $(BIN)/sphinx-build $(DOCS_SRC) $(DOCS_OUT) diff --git a/README.md b/README.md index 77848f4..3249765 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ blag is named after [the blag of the webcomic xkcd][blagxkcd]. ## Features * Write content in [Markdown][] +* Good looking default theme * Theming support using [Jinja2][] templates * Generation of Atom feeds for blog content * Fenced code blocks and syntax highlighting using [Pygments][] diff --git a/blag/blag.py b/blag/blag.py index 8ef4b9b..28fe983 100644 --- a/blag/blag.py +++ b/blag/blag.py @@ -6,32 +6,28 @@ # remove when we don't support py38 anymore from __future__ import annotations -from typing import Any + import argparse +import configparser +import logging import os import shutil -import logging -import configparser import sys +from typing import Any -from jinja2 import ( - Environment, - FileSystemLoader, - Template, - TemplateNotFound, -) import feedgenerator +from jinja2 import Environment, FileSystemLoader, Template, TemplateNotFound import blag -from blag.markdown import markdown_factory, convert_markdown from blag.devserver import serve -from blag.version import __VERSION__ +from blag.markdown import convert_markdown, markdown_factory from blag.quickstart import quickstart +from blag.version import __VERSION__ logger = logging.getLogger(__name__) logging.basicConfig( level=logging.INFO, - format='%(asctime)s %(levelname)s %(name)s %(message)s', + format="%(asctime)s %(levelname)s %(name)s %(message)s", ) @@ -70,84 +66,84 @@ def parse_args(args: list[str] | None = None) -> argparse.Namespace: """ parser = argparse.ArgumentParser() parser.add_argument( - '--version', - action='version', - version='%(prog)s ' + __VERSION__, + "--version", + action="version", + version="%(prog)s " + __VERSION__, ) parser.add_argument( - '-v', - '--verbose', - action='store_true', - help='Verbose output.', + "-v", + "--verbose", + action="store_true", + help="Verbose output.", ) - commands = parser.add_subparsers(dest='command') + commands = parser.add_subparsers(dest="command") commands.required = True build_parser = commands.add_parser( - 'build', - help='Build website.', + "build", + help="Build website.", ) build_parser.set_defaults(func=build) build_parser.add_argument( - '-i', - '--input-dir', - default='content', - help='Input directory (default: content)', + "-i", + "--input-dir", + default="content", + help="Input directory (default: content)", ) build_parser.add_argument( - '-o', - '--output-dir', - default='build', - help='Ouptut directory (default: build)', + "-o", + "--output-dir", + default="build", + help="Ouptut directory (default: build)", ) build_parser.add_argument( - '-t', - '--template-dir', - default='templates', - help='Template directory (default: templates)', + "-t", + "--template-dir", + default="templates", + help="Template directory (default: templates)", ) build_parser.add_argument( - '-s', - '--static-dir', - default='static', - help='Static directory (default: static)', + "-s", + "--static-dir", + default="static", + help="Static directory (default: static)", ) quickstart_parser = commands.add_parser( - 'quickstart', + "quickstart", help="Quickstart blag, creating necessary configuration.", ) quickstart_parser.set_defaults(func=quickstart) serve_parser = commands.add_parser( - 'serve', + "serve", help="Start development server.", ) serve_parser.set_defaults(func=serve) serve_parser.add_argument( - '-i', - '--input-dir', - default='content', - help='Input directory (default: content)', + "-i", + "--input-dir", + default="content", + help="Input directory (default: content)", ) serve_parser.add_argument( - '-o', - '--output-dir', - default='build', - help='Ouptut directory (default: build)', + "-o", + "--output-dir", + default="build", + help="Ouptut directory (default: build)", ) serve_parser.add_argument( - '-t', - '--template-dir', - default='templates', - help='Template directory (default: templates)', + "-t", + "--template-dir", + default="templates", + help="Template directory (default: templates)", ) serve_parser.add_argument( - '-s', - '--static-dir', - default='static', - help='Static directory (default: static)', + "-s", + "--static-dir", + default="static", + help="Static directory (default: static)", ) return parser.parse_args(args) @@ -170,18 +166,18 @@ def get_config(configfile: str) -> configparser.SectionProxy: config = configparser.ConfigParser() config.read(configfile) # check for the mandatory options - for value in 'base_url', 'title', 'description', 'author': + for value in "base_url", "title", "description", "author": try: - config['main'][value] + config["main"][value] except Exception: - print(f'{value} is missing in {configfile}!') + print(f"{value} is missing in {configfile}!") sys.exit(1) - if not config['main']['base_url'].endswith('/'): - logger.warning('base_url does not end with a slash, adding it.') - config['main']['base_url'] += '/' + if not config["main"]["base_url"].endswith("/"): + logger.warning("base_url does not end with a slash, adding it.") + config["main"]["base_url"] += "/" - return config['main'] + return config["main"] def environment_factory( @@ -222,50 +218,51 @@ def build(args: argparse.Namespace) -> None: args """ - os.makedirs(f'{args.output_dir}', exist_ok=True) + os.makedirs(f"{args.output_dir}", exist_ok=True) convertibles = [] for root, dirnames, filenames in os.walk(args.input_dir): for filename in filenames: rel_src = os.path.relpath( - f'{root}/{filename}', start=args.input_dir + f"{root}/{filename}", start=args.input_dir ) # all non-markdown files are just copied over, the markdown # files are converted to html - if rel_src.endswith('.md'): + if rel_src.endswith(".md"): rel_dst = rel_src - rel_dst = rel_dst[:-3] + '.html' + rel_dst = rel_dst[:-3] + ".html" convertibles.append((rel_src, rel_dst)) else: shutil.copy( - f'{args.input_dir}/{rel_src}', - f'{args.output_dir}/{rel_src}', + f"{args.input_dir}/{rel_src}", + f"{args.output_dir}/{rel_src}", ) for dirname in dirnames: # all directories are copied into the output directory - path = os.path.relpath(f'{root}/{dirname}', start=args.input_dir) - os.makedirs(f'{args.output_dir}/{path}', exist_ok=True) + path = os.path.relpath(f"{root}/{dirname}", start=args.input_dir) + os.makedirs(f"{args.output_dir}/{path}", exist_ok=True) # copy static files over - logger.info('Copying static files.') + logger.info("Copying static files.") if os.path.exists(args.static_dir): shutil.copytree(args.static_dir, args.output_dir, dirs_exist_ok=True) - config = get_config('config.ini') + config = get_config("config.ini") env = environment_factory(args.template_dir, dict(site=config)) try: - page_template = env.get_template('page.html') - article_template = env.get_template('article.html') - archive_template = env.get_template('archive.html') - tags_template = env.get_template('tags.html') - tag_template = env.get_template('tag.html') + page_template = env.get_template("page.html") + article_template = env.get_template("article.html") + index_template = env.get_template("index.html") + archive_template = env.get_template("archive.html") + tags_template = env.get_template("tags.html") + tag_template = env.get_template("tag.html") except TemplateNotFound as exc: - tmpl = os.path.join(blag.__path__[0], 'templates') + tmpl = os.path.join(blag.__path__[0], "templates") logger.error( f'Template "{exc.name}" not found in {args.template_dir}! ' - 'Consider running `blag quickstart` or copying the ' - f'missing template from {tmpl}.' + "Consider running `blag quickstart` or copying the " + f"missing template from {tmpl}." ) sys.exit(1) @@ -281,11 +278,12 @@ def build(args: argparse.Namespace) -> None: generate_feed( articles, args.output_dir, - base_url=config['base_url'], - blog_title=config['title'], - blog_description=config['description'], - blog_author=config['author'], + base_url=config["base_url"], + blog_title=config["title"], + blog_description=config["description"], + blog_author=config["author"], ) + generate_index(articles, index_template, args.output_dir) generate_archive(articles, archive_template, args.output_dir) generate_tags(articles, tags_template, tag_template, args.output_dir) @@ -305,6 +303,8 @@ def process_markdown( If a markdown file has a `date` metadata field it will be recognized as article otherwise as page. + Articles are sorted by date in descending order. + Parameters ---------- convertibles @@ -317,7 +317,7 @@ def process_markdown( Returns ------- list[tuple[str, dict[str, Any]]], list[tuple[str, dict[str, Any]]] - articles and pages + articles and pages, articles are sorted by date in descending order. """ logger.info("Converting Markdown files...") @@ -326,9 +326,9 @@ def process_markdown( articles = [] pages = [] 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() content, meta = convert_markdown(md, body) @@ -338,17 +338,17 @@ def process_markdown( # if markdown has date in meta, we treat it as a blog article, # everything else are just pages - if meta and 'date' in meta: + if meta and "date" in meta: articles.append((dst, context)) result = article_template.render(context) else: pages.append((dst, context)) result = page_template.render(context) - with open(f'{output_dir}/{dst}', 'w') as fh_dest: + with open(f"{output_dir}/{dst}", "w") as fh_dest: fh_dest.write(result) # sort articles by date, descending - articles = sorted(articles, key=lambda x: x[1]['date'], reverse=True) + articles = sorted(articles, key=lambda x: x[1]["date"], reverse=True) return articles, pages @@ -378,38 +378,40 @@ def generate_feed( blog author """ - logger.info('Generating Atom feed.') + logger.info("Generating Atom feed.") feed = feedgenerator.Atom1Feed( link=base_url, title=blog_title, description=blog_description, - feed_url=base_url + 'atom.xml', + feed_url=base_url + "atom.xml", ) for dst, context in articles: # if article has a description, use that. otherwise fall back to # the title - description = context.get('description', context['title']) + description = context.get("description", context["title"]) feed.add_item( - title=context['title'], + title=context["title"], author_name=blog_author, link=base_url + dst, description=description, - content=context['content'], - pubdate=context['date'], + content=context["content"], + pubdate=context["date"], ) - with open(f'{output_dir}/atom.xml', 'w') as fh: - feed.write(fh, encoding='utf8') + with open(f"{output_dir}/atom.xml", "w") as fh: + feed.write(fh, encoding="utf8") -def generate_archive( +def generate_index( articles: list[tuple[str, dict[str, Any]]], template: Template, output_dir: str, ) -> None: - """Generate the archive page. + """Generate the index page. + + This is used for the index (i.e. landing) page. Parameters ---------- @@ -423,11 +425,40 @@ def generate_archive( archive = [] for dst, context in articles: entry = context.copy() - entry['dst'] = dst + entry["dst"] = dst archive.append(entry) result = template.render(dict(archive=archive)) - with open(f'{output_dir}/index.html', 'w') as fh: + with open(f"{output_dir}/index.html", "w") as fh: + fh.write(result) + + +def generate_archive( + articles: list[tuple[str, dict[str, Any]]], + template: Template, + output_dir: str, +) -> None: + """Generate the archive page. + + This is used for the full archive. + + Parameters + ---------- + articles + List of articles. Each article has the destination path and a + dictionary with the content. + template + output_dir + + """ + archive = [] + for dst, context in articles: + entry = context.copy() + entry["dst"] = dst + archive.append(entry) + + result = template.render(dict(archive=archive)) + with open(f"{output_dir}/archive.html", "w") as fh: fh.write(result) @@ -449,11 +480,11 @@ def generate_tags( """ logger.info("Generating Tag-pages.") - os.makedirs(f'{output_dir}/tags', exist_ok=True) + os.makedirs(f"{output_dir}/tags", exist_ok=True) # get tags number of occurrences all_tags: dict[str, int] = {} for _, context in articles: - tags: list[str] = context.get('tags', []) + tags: list[str] = context.get("tags", []) for tag in tags: all_tags[tag] = all_tags.get(tag, 0) + 1 # sort by occurrence @@ -462,25 +493,25 @@ def generate_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) # get tags and archive per tag all_tags2: dict[str, list[dict[str, Any]]] = {} for dst, context in articles: - tags = context.get('tags', []) + tags = context.get("tags", []) for tag in tags: archive: list[dict[str, Any]] = all_tags2.get(tag, []) entry = context.copy() - entry['dst'] = dst + entry["dst"] = dst archive.append(entry) all_tags2[tag] = archive for tag, archive in all_tags2.items(): result = tag_template.render(dict(archive=archive, tag=tag)) - with open(f'{output_dir}/tags/{tag}.html', 'w') as fh: + with open(f"{output_dir}/tags/{tag}.html", "w") as fh: fh.write(result) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/blag/content/about.md b/blag/content/about.md new file mode 100644 index 0000000..4f01ccf --- /dev/null +++ b/blag/content/about.md @@ -0,0 +1,8 @@ +title: About Me +description: Short description of this page. + + +## About Me + +This is a regular page, i.e. not a blog post. Feel free to delete this page, +populate it with more content or generate more [pages like this](testpage.md). diff --git a/blag/content/hello-world.md b/blag/content/hello-world.md new file mode 100644 index 0000000..7039dd9 --- /dev/null +++ b/blag/content/hello-world.md @@ -0,0 +1,51 @@ +Title: Hello World! +Description: Hello there, this is the first blog post. You should read me first. +Date: 2023-01-01 12:00 +Tags: blag, pygments + + +## Hello World + +This is an example blog post. Internally, blag differentiates between **pages** +and **articles**. Intuitively, pages are simple pages and articles are blog +posts. The decision whether a document is a page or an article is made +depending on the presence of the `date` metadata element: Any document that +contains the `date` metadata element is an article, everything else a page. + +This differentiation has consequences: + +* blag uses different templates: `page.html` and `article.html` +* only articles are collected in the Atom feed +* only articles are aggregated in the tag pages + +For more detailed information, please refer to the [documentation][doc] + +[doc]: https://blag.readthedocs.io + + +### Syntax Highlighting + +```python +def foo(bar): + """This is a docstring. + + """ + # comment + return bar +``` + +Syntax highlighting is done via [Pygments][pygments]. For code blocks, blag +generates the necessary CSS classes by default, which you can use to style your +code using CSS. It provides you with a default light- and dark theme, for more +information on how to generate a different theme, please refer to [Pygments' +documentation][pygments]. + +[pygments]: https://pygments.org + + +### Next Steps + +* Adapt the files in `templates` to your needs +* Check out the files in `static` and modify as needed +* Add some content +* Change the [favicon.ico](favicon.ico) diff --git a/blag/content/second-post.md b/blag/content/second-post.md new file mode 100644 index 0000000..5a35d4b --- /dev/null +++ b/blag/content/second-post.md @@ -0,0 +1,9 @@ +Title: Second Post +Description: This is the second blog post, so you can see how it looks like on the front page. +Date: 2023-01-02 12:00 +Tags: blag + + +## Second Post + +This page serves no purpose :) diff --git a/blag/content/testpage.md b/blag/content/testpage.md new file mode 100644 index 0000000..24fe02b --- /dev/null +++ b/blag/content/testpage.md @@ -0,0 +1,46 @@ +# This Is A Headline + +This is some **bold text** with some `code` inside. This is _some_underlined_ +text with some `code` inside. This is some text with some `code` inside. This +is some text with some `code` inside. This is some text with some `code` +inside. This is some text with some `code` inside. This is some text with some +`code` inside. This is some text with some `code` inside. + +This is some [link](https://example.com) inside the text -- it does not really +lead anywhere! This is some [link](https://example.com) inside the text -- it +does not really lead anywhere! This is some [link](https://example.com) inside +the text -- it does not really lead anywhere! + + +* some bullets +* some other + * bullets + * foo + +```python +# this is some python code + +class Foo: + + def __init__(self, foo, bar): + self.foo = foo + self.bar = bar + + def do_something(): + """This is the docstring of this method. + + """ + return foo +``` + + +## Some other headline + +This is some other text + +```makefile + +# some comment +foo: + ls -lh +``` diff --git a/blag/devserver.py b/blag/devserver.py index a57ad71..4f3ec50 100644 --- a/blag/devserver.py +++ b/blag/devserver.py @@ -8,18 +8,18 @@ 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 +import logging +import multiprocessing +import os +import time +from functools import partial +from http.server import HTTPServer, SimpleHTTPRequestHandler +from typing import NoReturn from blag import blag - logger = logging.getLogger(__name__) @@ -69,7 +69,7 @@ def autoreload(args: argparse.Namespace) -> NoReturn: """ dirs = [args.input_dir, args.template_dir, args.static_dir] - logger.info(f'Monitoring {dirs} for changes...') + logger.info(f"Monitoring {dirs} for changes...") # make sure we trigger the rebuild immediately when we enter the # loop to avoid serving stale contents last_mtime = 0.0 @@ -77,7 +77,7 @@ def autoreload(args: argparse.Namespace) -> NoReturn: mtime = get_last_modified(dirs) if mtime > last_mtime: last_mtime = mtime - logger.info('Change detected, rebuilding...') + logger.info("Change detected, rebuilding...") blag.build(args) time.sleep(1) @@ -92,7 +92,7 @@ def serve(args: argparse.Namespace) -> None: """ httpd = HTTPServer( - ('', 8000), + ("", 8000), partial(SimpleHTTPRequestHandler, directory=args.output_dir), ) proc = multiprocessing.Process(target=autoreload, args=(args,)) diff --git a/blag/markdown.py b/blag/markdown.py index 6b055ed..e23d1e9 100644 --- a/blag/markdown.py +++ b/blag/markdown.py @@ -7,8 +7,9 @@ processing. # remove when we don't support py38 anymore from __future__ import annotations -from datetime import datetime + import logging +from datetime import datetime from urllib.parse import urlsplit, urlunsplit from xml.etree.ElementTree import Element @@ -16,7 +17,6 @@ from markdown import Markdown from markdown.extensions import Extension from markdown.treeprocessors import Treeprocessor - logger = logging.getLogger(__name__) @@ -33,13 +33,13 @@ def markdown_factory() -> Markdown: """ md = Markdown( extensions=[ - 'meta', - 'fenced_code', - 'codehilite', - 'smarty', + "meta", + "fenced_code", + "codehilite", + "smarty", MarkdownLinkExtension(), ], - output_format='html', + output_format="html", ) return md @@ -75,20 +75,20 @@ def convert_markdown( # markdowns metadata consists as list of strings -- one item per # line. let's convert into single strings. for key, value in meta.items(): - value = '\n'.join(value) + value = "\n".join(value) meta[key] = value # convert known metadata # date: datetime - if 'date' in meta: - meta['date'] = datetime.fromisoformat(meta['date']) - meta['date'] = meta['date'].astimezone() + if "date" in meta: + meta["date"] = datetime.fromisoformat(meta["date"]) + meta["date"] = meta["date"].astimezone() # tags: list[str] and lower case - if 'tags' in meta: - tags = meta['tags'].split(',') + if "tags" in meta: + tags = meta["tags"].split(",") tags = [t.lower() for t in tags] tags = [t.strip() for t in tags] - meta['tags'] = tags + meta["tags"] = tags return content, meta @@ -98,25 +98,25 @@ class MarkdownLinkTreeprocessor(Treeprocessor): def run(self, root: Element) -> Element: for element in root.iter(): - if element.tag == 'a': - url = element.get('href') + if element.tag == "a": + url = element.get("href") # element.get could also return None, we haven't seen this so # far, so lets wait if we raise this assert url is not None url = str(url) converted = self.convert(url) - element.set('href', converted) + element.set("href", converted) return root def convert(self, url: str) -> str: scheme, netloc, path, query, fragment = urlsplit(url) logger.debug( - f'{url}: {scheme=} {netloc=} {path=} {query=} {fragment=}' + f"{url}: {scheme=} {netloc=} {path=} {query=} {fragment=}" ) if scheme or netloc or not path: return url - if path.endswith('.md'): - path = path[:-3] + '.html' + if path.endswith(".md"): + path = path[:-3] + ".html" url = urlunsplit((scheme, netloc, path, query, fragment)) return url @@ -128,6 +128,6 @@ class MarkdownLinkExtension(Extension): def extendMarkdown(self, md: Markdown) -> None: md.treeprocessors.register( MarkdownLinkTreeprocessor(md), - 'mdlink', + "mdlink", 0, ) diff --git a/blag/quickstart.py b/blag/quickstart.py index aee73e1..42e8425 100644 --- a/blag/quickstart.py +++ b/blag/quickstart.py @@ -4,10 +4,11 @@ # remove when we don't support py38 anymore from __future__ import annotations -import configparser + import argparse -import shutil +import configparser import os +import shutil import blag @@ -37,27 +38,33 @@ def get_input(question: str, default: str) -> str: return reply -def copy_templates() -> None: - """Copy templates into current directory. +def copy_default_theme() -> None: + """Copy default theme into current directory. + + The default theme contains the 'templates', 'content' and 'static' + directories shipped with blag. It will not overwrite existing files. """ - print("Copying templates...") - try: - shutil.copytree( - os.path.join(blag.__path__[0], 'templates'), - 'templates', - ) - except FileExistsError: - print("Templates already exist. Skipping.") + print("Copying default theme...") + for dir_ in "templates", "content", "static": + print(f" Copying {dir_}...") + try: + shutil.copytree( + os.path.join(blag.__path__[0], dir_), + dir_, + ) + except FileExistsError: + print(f" {dir_} already exist. Skipping.") def quickstart(args: argparse.Namespace | None) -> None: """Quickstart. - This method asks the user some questions and generates a - configuration file that is needed in order to run blag. + This method asks the user some questions and generates a configuration file + that is needed in order to run blag. Additionally, it creates the content + and static directories with some initial content, to get the user started. Parameters ---------- @@ -83,13 +90,13 @@ def quickstart(args: argparse.Namespace | None) -> None: ) config = configparser.ConfigParser() - config['main'] = { - 'base_url': base_url, - 'title': title, - 'description': description, - 'author': author, + config["main"] = { + "base_url": base_url, + "title": title, + "description": description, + "author": author, } - with open('config.ini', 'w') as fh: + with open("config.ini", "w") as fh: config.write(fh) - copy_templates() + copy_default_theme() diff --git a/blag/static/code-dark.css b/blag/static/code-dark.css new file mode 100644 index 0000000..a2af57e --- /dev/null +++ b/blag/static/code-dark.css @@ -0,0 +1,83 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.hll { background-color: #49483e } +.c { color: #75715e } /* Comment */ +.err { color: #960050; background-color: #1e0010 } /* Error */ +.esc { color: #f8f8f2 } /* Escape */ +.g { color: #f8f8f2 } /* Generic */ +.k { color: #66d9ef } /* Keyword */ +.l { color: #ae81ff } /* Literal */ +.n { color: #f8f8f2 } /* Name */ +.o { color: #f92672 } /* Operator */ +.x { color: #f8f8f2 } /* Other */ +.p { color: #f8f8f2 } /* Punctuation */ +.ch { color: #75715e } /* Comment.Hashbang */ +.cm { color: #75715e } /* Comment.Multiline */ +.cp { color: #75715e } /* Comment.Preproc */ +.cpf { color: #75715e } /* Comment.PreprocFile */ +.c1 { color: #75715e } /* Comment.Single */ +.cs { color: #75715e } /* Comment.Special */ +.gd { color: #f92672 } /* Generic.Deleted */ +.ge { color: #f8f8f2; font-style: italic } /* Generic.Emph */ +.gr { color: #f8f8f2 } /* Generic.Error */ +.gh { color: #f8f8f2 } /* Generic.Heading */ +.gi { color: #a6e22e } /* Generic.Inserted */ +.go { color: #66d9ef } /* Generic.Output */ +.gp { color: #f92672; font-weight: bold } /* Generic.Prompt */ +.gs { color: #f8f8f2; font-weight: bold } /* Generic.Strong */ +.gu { color: #75715e } /* Generic.Subheading */ +.gt { color: #f8f8f2 } /* Generic.Traceback */ +.kc { color: #66d9ef } /* Keyword.Constant */ +.kd { color: #66d9ef } /* Keyword.Declaration */ +.kn { color: #f92672 } /* Keyword.Namespace */ +.kp { color: #66d9ef } /* Keyword.Pseudo */ +.kr { color: #66d9ef } /* Keyword.Reserved */ +.kt { color: #66d9ef } /* Keyword.Type */ +.ld { color: #e6db74 } /* Literal.Date */ +.m { color: #ae81ff } /* Literal.Number */ +.s { color: #e6db74 } /* Literal.String */ +.na { color: #a6e22e } /* Name.Attribute */ +.nb { color: #f8f8f2 } /* Name.Builtin */ +.nc { color: #a6e22e } /* Name.Class */ +.no { color: #66d9ef } /* Name.Constant */ +.nd { color: #a6e22e } /* Name.Decorator */ +.ni { color: #f8f8f2 } /* Name.Entity */ +.ne { color: #a6e22e } /* Name.Exception */ +.nf { color: #a6e22e } /* Name.Function */ +.nl { color: #f8f8f2 } /* Name.Label */ +.nn { color: #f8f8f2 } /* Name.Namespace */ +.nx { color: #a6e22e } /* Name.Other */ +.py { color: #f8f8f2 } /* Name.Property */ +.nt { color: #f92672 } /* Name.Tag */ +.nv { color: #f8f8f2 } /* Name.Variable */ +.ow { color: #f92672 } /* Operator.Word */ +.pm { color: #f8f8f2 } /* Punctuation.Marker */ +.w { color: #f8f8f2 } /* Text.Whitespace */ +.mb { color: #ae81ff } /* Literal.Number.Bin */ +.mf { color: #ae81ff } /* Literal.Number.Float */ +.mh { color: #ae81ff } /* Literal.Number.Hex */ +.mi { color: #ae81ff } /* Literal.Number.Integer */ +.mo { color: #ae81ff } /* Literal.Number.Oct */ +.sa { color: #e6db74 } /* Literal.String.Affix */ +.sb { color: #e6db74 } /* Literal.String.Backtick */ +.sc { color: #e6db74 } /* Literal.String.Char */ +.dl { color: #e6db74 } /* Literal.String.Delimiter */ +.sd { color: #e6db74 } /* Literal.String.Doc */ +.s2 { color: #e6db74 } /* Literal.String.Double */ +.se { color: #ae81ff } /* Literal.String.Escape */ +.sh { color: #e6db74 } /* Literal.String.Heredoc */ +.si { color: #e6db74 } /* Literal.String.Interpol */ +.sx { color: #e6db74 } /* Literal.String.Other */ +.sr { color: #e6db74 } /* Literal.String.Regex */ +.s1 { color: #e6db74 } /* Literal.String.Single */ +.ss { color: #e6db74 } /* Literal.String.Symbol */ +.bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */ +.fm { color: #a6e22e } /* Name.Function.Magic */ +.vc { color: #f8f8f2 } /* Name.Variable.Class */ +.vg { color: #f8f8f2 } /* Name.Variable.Global */ +.vi { color: #f8f8f2 } /* Name.Variable.Instance */ +.vm { color: #f8f8f2 } /* Name.Variable.Magic */ +.il { color: #ae81ff } /* Literal.Number.Integer.Long */ diff --git a/blag/static/code-light.css b/blag/static/code-light.css new file mode 100644 index 0000000..e2cc7b8 --- /dev/null +++ b/blag/static/code-light.css @@ -0,0 +1,73 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.hll { background-color: #ffffcc } +.c { color: #3D7B7B; font-style: italic } /* Comment */ +.err { border: 1px solid #FF0000 } /* Error */ +.k { color: #008000; font-weight: bold } /* Keyword */ +.o { color: #666666 } /* Operator */ +.ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */ +.cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */ +.cp { color: #9C6500 } /* Comment.Preproc */ +.cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */ +.c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */ +.cs { color: #3D7B7B; font-style: italic } /* Comment.Special */ +.gd { color: #A00000 } /* Generic.Deleted */ +.ge { font-style: italic } /* Generic.Emph */ +.gr { color: #E40000 } /* Generic.Error */ +.gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.gi { color: #008400 } /* Generic.Inserted */ +.go { color: #717171 } /* Generic.Output */ +.gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.gs { font-weight: bold } /* Generic.Strong */ +.gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.gt { color: #0044DD } /* Generic.Traceback */ +.kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +.kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +.kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ +.kp { color: #008000 } /* Keyword.Pseudo */ +.kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +.kt { color: #B00040 } /* Keyword.Type */ +.m { color: #666666 } /* Literal.Number */ +.s { color: #BA2121 } /* Literal.String */ +.na { color: #687822 } /* Name.Attribute */ +.nb { color: #008000 } /* Name.Builtin */ +.nc { color: #0000FF; font-weight: bold } /* Name.Class */ +.no { color: #880000 } /* Name.Constant */ +.nd { color: #AA22FF } /* Name.Decorator */ +.ni { color: #717171; font-weight: bold } /* Name.Entity */ +.ne { color: #CB3F38; font-weight: bold } /* Name.Exception */ +.nf { color: #0000FF } /* Name.Function */ +.nl { color: #767600 } /* Name.Label */ +.nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +.nt { color: #008000; font-weight: bold } /* Name.Tag */ +.nv { color: #19177C } /* Name.Variable */ +.ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +.w { color: #bbbbbb } /* Text.Whitespace */ +.mb { color: #666666 } /* Literal.Number.Bin */ +.mf { color: #666666 } /* Literal.Number.Float */ +.mh { color: #666666 } /* Literal.Number.Hex */ +.mi { color: #666666 } /* Literal.Number.Integer */ +.mo { color: #666666 } /* Literal.Number.Oct */ +.sa { color: #BA2121 } /* Literal.String.Affix */ +.sb { color: #BA2121 } /* Literal.String.Backtick */ +.sc { color: #BA2121 } /* Literal.String.Char */ +.dl { color: #BA2121 } /* Literal.String.Delimiter */ +.sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.s2 { color: #BA2121 } /* Literal.String.Double */ +.se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ +.sh { color: #BA2121 } /* Literal.String.Heredoc */ +.si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ +.sx { color: #008000 } /* Literal.String.Other */ +.sr { color: #A45A77 } /* Literal.String.Regex */ +.s1 { color: #BA2121 } /* Literal.String.Single */ +.ss { color: #19177C } /* Literal.String.Symbol */ +.bp { color: #008000 } /* Name.Builtin.Pseudo */ +.fm { color: #0000FF } /* Name.Function.Magic */ +.vc { color: #19177C } /* Name.Variable.Class */ +.vg { color: #19177C } /* Name.Variable.Global */ +.vi { color: #19177C } /* Name.Variable.Instance */ +.vm { color: #19177C } /* Name.Variable.Magic */ +.il { color: #666666 } /* Literal.Number.Integer.Long */ diff --git a/blag/static/favicon.ico b/blag/static/favicon.ico new file mode 100644 index 0000000..6175ffc Binary files /dev/null and b/blag/static/favicon.ico differ diff --git a/blag/static/style.css b/blag/static/style.css new file mode 100644 index 0000000..416284a --- /dev/null +++ b/blag/static/style.css @@ -0,0 +1,166 @@ +@import "code-light.css" (prefers-color-scheme: light); +@import "code-dark.css" (prefers-color-scheme: dark); + +@media (prefers-color-scheme: light) { + :root { + --background: #FFFFFF; + --background-dim: #f5f7f9; + + --foreground: #2B303A; + --foreground-dim: #576379; + --foreground-heavy: #191C22; + + --primary-color: #375287; + } +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #2B363B; + --background-dim: #2F3C42; + + --foreground: #f0f2f3; + --foreground-dim: #d5d5d5; + --foreground-heavy: #f2f4f5; + + --primary-color: #A1C5FF; + } +} + + +html { + font-size: 18px; + font-family: serif; +} + +body { + margin: 0 auto; + max-width: 50rem; + background: var(--background); + color: var(--foreground); + line-height: 1.5; + padding: 0rem 0.5rem; +} + +aside { + font-size: smaller; + font-style: italic; + color: var(--foreground-dim); +} + +h1, +h2, +h3, +h4, +h5, +h6, +strong { + color: var(--foreground-heavy); +} + +a { + color: var(--primary-color); +} + +h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { + text-decoration: none; +} + +h1 a:hover, h2 a:hover, h3 a:hover, h4 a:hover, h5 a:hover, h6 a:hover { + text-decoration: underline; +} + +nav ul { + list-style: none; +} + +nav li { + display: inline; +} + +nav li + li:before { + content: " · "; + margin: 0 0.5ex; +} + +article header { + display: flex; + flex-direction: row; + margin: 1rem 0; +} + +article header time { + white-space: nowrap; + color: var(--foreground-dim); + font-style: italic; + flex: 0 0 12ex; +} + +article header h2, +article header p { + font-size: 1rem; + display: inline; +} + +code, +pre { + background: var(--background-dim); + border-radius: 0.3rem; + font-family: monospace; +} + +pre { + padding: 1rem; + border-left: 2px solid var(--primary-color); + overflow: auto; +} + +code { + padding: 0.1rem 0.2rem; +} + +/* reset the padding for code inside pre */ +pre code { + padding: 0; +} + +blockquote { + background: var(--background-dim); + border-radius: 0 0.3rem 0.3rem 0; + font-style: italic; + border-left: 2px solid var(--primary-color); + margin: 0; + padding: 1rem; +} + +/* reset the margin for p inside blockquotes */ +blockquote p { + margin: 0; +} + +body > header { + padding: 2rem 0; +} + +body footer { + margin: 3rem 0; + color: var(--foreground-dim); + font-size: smaller; + text-align: center; +} + +header nav { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +header h1 { + margin: 0 auto; + color: var(--primary-color); +} + +header h2 { + display: inline; + font-size: 1.2rem; +} diff --git a/blag/templates/archive.html b/blag/templates/archive.html index e8f9cca..5109438 100644 --- a/blag/templates/archive.html +++ b/blag/templates/archive.html @@ -1,20 +1,23 @@ {% extends "base.html" %} -{% block title %}{{ site.title }}{% endblock %} +{% block title %}Archive{% endblock %} {% block content %} + {% for entry in archive %} - {% if entry.title %} -

{{entry.title}}

- - {% if entry.description %} -

— {{ entry.description }}

- {% endif %} - - {% endif %} - -

Written on {{ entry.date.date() }}.

+
+
+ +
+

{{ entry.title }}

+ {% if entry.description %} +

— {{ entry.description }}

+ {% endif %} +
+
+
{% endfor %} + {% endblock %} diff --git a/blag/templates/article.html b/blag/templates/article.html index 4080c36..889186d 100644 --- a/blag/templates/article.html +++ b/blag/templates/article.html @@ -22,7 +22,6 @@

- {{ content }} {% endblock %} diff --git a/blag/templates/base.html b/blag/templates/base.html index fc6f822..2dad0d7 100644 --- a/blag/templates/base.html +++ b/blag/templates/base.html @@ -4,12 +4,15 @@ + {%- if description %} {%- else %} {%- endif %} + + {% block title %}{% endblock %} | {{ site.description }} @@ -19,9 +22,10 @@ @@ -31,7 +35,16 @@ {% endblock %} + - diff --git a/blag/templates/index.html b/blag/templates/index.html new file mode 100644 index 0000000..002e185 --- /dev/null +++ b/blag/templates/index.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block title %}{{ site.title }}{% endblock %} + +{% block content %} + +{% for entry in archive[:15] %} + +
+
+ +
+

{{ entry.title }}

+ {% if entry.description %} +

— {{ entry.description }}

+ {% endif %} +
+
+
+ +{% endfor %} + +

all articles...

+ +{% endblock %} diff --git a/blag/templates/page.html b/blag/templates/page.html index 5507a42..bdeab2b 100644 --- a/blag/templates/page.html +++ b/blag/templates/page.html @@ -3,5 +3,7 @@ {% block title %}{{ title }}{% endblock %} {% block content %} -{{ content }} + + {{ content }} + {% endblock %} diff --git a/blag/templates/tag.html b/blag/templates/tag.html index 7abb4df..4abd3bc 100644 --- a/blag/templates/tag.html +++ b/blag/templates/tag.html @@ -1,20 +1,25 @@ {% extends "base.html" %} -{% block title %}Tag {{ tag }}{% endblock %} +{% block title %}#{{ tag }}{% endblock %} {% block content %} + +

Articles tagged "{{ tag }}"

+ {% for entry in archive %} - {% if entry.title %} -

{{entry.title}}

- - {% if entry.description %} -

— {{ entry.description }}

- {% endif %} - - {% endif %} - -

Written on {{ entry.date.date() }}.

+
+
+ +
+

{{ entry.title }}

+ {% if entry.description %} +

— {{ entry.description }}

+ {% endif %} +
+
+
{% endfor %} + {% endblock %} diff --git a/blag/version.py b/blag/version.py index 3fe4bea..7e6a9cc 100644 --- a/blag/version.py +++ b/blag/version.py @@ -1 +1 @@ -__VERSION__ = '1.5.0' +__VERSION__ = "1.5.0" diff --git a/docs/blag.rst b/docs/blag.rst index 2c7ac0c..92239c9 100644 --- a/docs/blag.rst +++ b/docs/blag.rst @@ -13,7 +13,8 @@ Install blag from PyPI_ .. _pypi: https://pypi.org/project/blag/ -Run blag's quickstart command to create the configuration and templates needed +Run blag's quickstart command to create the configuration, templates and some +initial content. .. code-block:: sh @@ -23,7 +24,6 @@ Create some content .. code-block:: sh - $ mkdir content $ edit content/hello-world.md Generate the website @@ -121,7 +121,7 @@ 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 +content and static files, you can use the ``static`` directory and put the files there. All files and directories found in the ``static`` directory will be copied over to ``build``. @@ -193,7 +193,8 @@ 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 +index.html landing page of the blog site, archive +archive.html archive page of the blog site, archive tags.html list of tags site, tags tag.html archive of Articles with a certain tag site, archive, tag ============ ====================================== =================== diff --git a/pyproject.toml b/pyproject.toml index 85dbb8e..e6b5dda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,11 +47,15 @@ version = {attr = "blag.__VERSION__" } [tool.setuptools] packages = [ "blag", - "blag.templates", + "tests", ] [tool.setuptools.package-data] -blag = ["templates/*"] +blag = [ + "templates/*", + "static/*", + "content/*", +] [tool.pytest.ini_options] addopts = """ diff --git a/requirements-dev.txt b/requirements-dev.txt index 2c806f0..0cf243c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,11 +1,11 @@ -build==0.9.0 +build==0.10.0 mkdocs==1.4.3 mkdocs-material==9.1.15 mkdocstrings[python]==0.20.0 twine==4.0.2 wheel==0.40.0 -pytest==7.3.0 +pytest==7.3.2 pytest-cov==4.0.0 flake8==6.0.0 mypy==1.2.0 -types-markdown==3.4.2.1 +types-markdown==3.4.2.9 diff --git a/requirements.txt b/requirements.txt index 58367ba..6607ece 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -markdown==3.4.1 +markdown==3.4.3 feedgenerator==2.0.0 jinja2==3.1.2 -pygments==2.13.0 +pygments==2.15.1 diff --git a/tests/conftest.py b/tests/conftest.py index 4f49918..f852463 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,10 @@ # 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 +from argparse import Namespace +from tempfile import TemporaryDirectory +from typing import Callable, Iterator import pytest from jinja2 import Environment, Template @@ -14,38 +15,43 @@ from blag import blag, quickstart @pytest.fixture def environment(cleandir: str) -> Iterator[Environment]: site = { - 'base_url': 'site base_url', - 'title': 'site title', - 'description': 'site description', - 'author': 'site author', + "base_url": "site base_url", + "title": "site title", + "description": "site description", + "author": "site author", } - env = blag.environment_factory('templates', globals_=dict(site=site)) + env = blag.environment_factory("templates", globals_=dict(site=site)) yield env @pytest.fixture def page_template(environment: Environment) -> Iterator[Template]: - yield environment.get_template('page.html') + yield environment.get_template("page.html") @pytest.fixture def article_template(environment: Environment) -> Iterator[Template]: - yield environment.get_template('article.html') + yield environment.get_template("article.html") + + +@pytest.fixture +def index_template(environment: Environment) -> Iterator[Template]: + yield environment.get_template("index.html") @pytest.fixture def archive_template(environment: Environment) -> Iterator[Template]: - yield environment.get_template('archive.html') + yield environment.get_template("archive.html") @pytest.fixture def tags_template(environment: Environment) -> Iterator[Template]: - yield environment.get_template('tags.html') + yield environment.get_template("tags.html") @pytest.fixture def tag_template(environment: Environment) -> Iterator[Template]: - yield environment.get_template('tag.html') + yield environment.get_template("tag.html") @pytest.fixture @@ -60,14 +66,13 @@ author = a. u. thor """ with TemporaryDirectory() as dir: - for d in 'content', 'build', 'static': - os.mkdir(f'{dir}/{d}') - with open(f'{dir}/config.ini', 'w') as fh: + os.mkdir(f"{dir}/build") + with open(f"{dir}/config.ini", "w") as fh: fh.write(config) # change directory old_cwd = os.getcwd() os.chdir(dir) - quickstart.copy_templates() + quickstart.copy_default_theme() yield dir # and change back afterwards os.chdir(old_cwd) @@ -75,11 +80,10 @@ author = a. u. thor @pytest.fixture def args(cleandir: Callable[[], Iterator[str]]) -> Iterator[Namespace]: - args = Namespace( - input_dir='content', - output_dir='build', - static_dir='static', - template_dir='templates', + input_dir="content", + output_dir="build", + static_dir="static", + template_dir="templates", ) yield args diff --git a/tests/test_blag.py b/tests/test_blag.py index 56cd153..e454ca1 100644 --- a/tests/test_blag.py +++ b/tests/test_blag.py @@ -1,73 +1,73 @@ # remove when we don't support py38 anymore from __future__ import annotations -from tempfile import TemporaryDirectory + import os -from datetime import datetime -from typing import Any from argparse import Namespace +from datetime import datetime +from tempfile import TemporaryDirectory +from typing import Any import pytest -from pytest import CaptureFixture, LogCaptureFixture from jinja2 import Template +from pytest import CaptureFixture, LogCaptureFixture -from blag import __VERSION__ -from blag import blag +from blag import __VERSION__, blag def test_generate_feed(cleandir: str) -> None: articles: list[tuple[str, dict[str, Any]]] = [] - blag.generate_feed(articles, 'build', ' ', ' ', ' ', ' ') - assert os.path.exists('build/atom.xml') + blag.generate_feed(articles, "build", " ", " ", " ", " ") + assert os.path.exists("build/atom.xml") def test_feed(cleandir: str) -> None: articles: list[tuple[str, dict[str, Any]]] = [ ( - 'dest1.html', + "dest1.html", { - 'title': 'title1', - 'date': datetime(2019, 6, 6), - 'content': 'content1', + "title": "title1", + "date": datetime(2019, 6, 6), + "content": "content1", }, ), ( - 'dest2.html', + "dest2.html", { - 'title': 'title2', - 'date': datetime(1980, 5, 9), - 'content': 'content2', + "title": "title2", + "date": datetime(1980, 5, 9), + "content": "content2", }, ), ] blag.generate_feed( articles, - 'build', - 'https://example.com/', - 'blog title', - 'blog description', - 'blog author', + "build", + "https://example.com/", + "blog title", + "blog description", + "blog author", ) - with open('build/atom.xml') as fh: + with open("build/atom.xml") as fh: feed = fh.read() - assert 'blog title' in feed + assert "blog title" in feed # enable when https://github.com/getpelican/feedgenerator/issues/22 # is fixed # assert 'blog description' in feed - assert 'blog author' in feed + assert "blog author" in feed # article 1 - assert 'title1' in feed + assert "title1" in feed assert 'title1' in feed - assert '2019-06-06' in feed + assert "2019-06-06" in feed assert 'content1' in feed assert 'title2' in feed + assert "title2" in feed assert 'title2' in feed - assert '1980-05-09' in feed + assert "1980-05-09" in feed assert 'content2' in feed assert ' None: # the feed, otherwise we simply use the title of the article articles: list[tuple[str, dict[str, Any]]] = [ ( - 'dest.html', + "dest.html", { - 'title': 'title', - 'description': 'description', - 'date': datetime(2019, 6, 6), - 'content': 'content', + "title": "title", + "description": "description", + "date": datetime(2019, 6, 6), + "content": "content", }, ) ] - blag.generate_feed(articles, 'build', ' ', ' ', ' ', ' ') + blag.generate_feed(articles, "build", " ", " ", " ", " ") - with open('build/atom.xml') as fh: + with open("build/atom.xml") as fh: feed = fh.read() - assert 'title' in feed + assert "title" in feed assert 'description' in feed - assert '2019-06-06' in feed + assert "2019-06-06" in feed assert 'content' in feed def test_parse_args_build() -> None: # test default args - args = blag.parse_args(['build']) - assert args.input_dir == 'content' - assert args.output_dir == 'build' - assert args.template_dir == 'templates' - assert args.static_dir == 'static' + args = blag.parse_args(["build"]) + assert args.input_dir == "content" + assert args.output_dir == "build" + assert args.template_dir == "templates" + assert args.static_dir == "static" # input dir - args = blag.parse_args(['build', '-i', 'foo']) - assert args.input_dir == 'foo' - args = blag.parse_args(['build', '--input-dir', 'foo']) - assert args.input_dir == 'foo' + args = blag.parse_args(["build", "-i", "foo"]) + assert args.input_dir == "foo" + args = blag.parse_args(["build", "--input-dir", "foo"]) + assert args.input_dir == "foo" # output dir - args = blag.parse_args(['build', '-o', 'foo']) - assert args.output_dir == 'foo' - args = blag.parse_args(['build', '--output-dir', 'foo']) - assert args.output_dir == 'foo' + args = blag.parse_args(["build", "-o", "foo"]) + assert args.output_dir == "foo" + args = blag.parse_args(["build", "--output-dir", "foo"]) + assert args.output_dir == "foo" # template dir - args = blag.parse_args(['build', '-t', 'foo']) - assert args.template_dir == 'foo' - args = blag.parse_args(['build', '--template-dir', 'foo']) - assert args.template_dir == 'foo' + args = blag.parse_args(["build", "-t", "foo"]) + assert args.template_dir == "foo" + args = blag.parse_args(["build", "--template-dir", "foo"]) + assert args.template_dir == "foo" # static dir - args = blag.parse_args(['build', '-s', 'foo']) - assert args.static_dir == 'foo' - args = blag.parse_args(['build', '--static-dir', 'foo']) - assert args.static_dir == 'foo' + args = blag.parse_args(["build", "-s", "foo"]) + assert args.static_dir == "foo" + args = blag.parse_args(["build", "--static-dir", "foo"]) + assert args.static_dir == "foo" def test_get_config() -> None: @@ -140,24 +140,24 @@ author = a. u. thor """ # happy path with TemporaryDirectory() as dir: - configfile = f'{dir}/config.ini' - with open(configfile, 'w') as fh: + configfile = f"{dir}/config.ini" + with open(configfile, "w") as fh: fh.write(config) config_parsed = blag.get_config(configfile) - assert config_parsed['base_url'] == 'https://example.com/' - assert config_parsed['title'] == 'title' - assert config_parsed['description'] == 'description' - assert config_parsed['author'] == 'a. u. thor' + assert config_parsed["base_url"] == "https://example.com/" + assert config_parsed["title"] == "title" + assert config_parsed["description"] == "description" + assert config_parsed["author"] == "a. u. thor" # a missing required config causes a sys.exit - for x in 'base_url', 'title', 'description', 'author': - config2 = '\n'.join( + for x in "base_url", "title", "description", "author": + config2 = "\n".join( [line for line in config.splitlines() if not line.startswith(x)] ) with TemporaryDirectory() as dir: - configfile = f'{dir}/config.ini' - with open(configfile, 'w') as fh: + configfile = f"{dir}/config.ini" + with open(configfile, "w") as fh: fh.write(config2) with pytest.raises(SystemExit): config_parsed = blag.get_config(configfile) @@ -171,19 +171,19 @@ description = description author = a. u. thor """ with TemporaryDirectory() as dir: - configfile = f'{dir}/config.ini' - with open(configfile, 'w') as fh: + configfile = f"{dir}/config.ini" + with open(configfile, "w") as fh: fh.write(config) config_parsed = blag.get_config(configfile) - assert config_parsed['base_url'] == 'https://example.com/' + assert config_parsed["base_url"] == "https://example.com/" def test_environment_factory(cleandir: str) -> None: - globals_: dict[str, object] = {'foo': 'bar', 'test': 'me'} + globals_: dict[str, object] = {"foo": "bar", "test": "me"} env = blag.environment_factory("templates", globals_=globals_) - assert env.globals['foo'] == 'bar' - assert env.globals['test'] == 'me' + assert env.globals["foo"] == "bar" + assert env.globals["test"] == "me" def test_process_markdown( @@ -216,12 +216,12 @@ foo bar convertibles = [] for i, txt in enumerate((page1, article1, article2)): - with open(f'content/{str(i)}', 'w') as fh: + with open(f"content/{str(i)}", "w") as fh: fh.write(txt) convertibles.append((str(i), str(i))) articles, pages = blag.process_markdown( - convertibles, 'content', 'build', page_template, article_template + convertibles, "content", "build", page_template, article_template ) assert isinstance(articles, list) @@ -229,14 +229,14 @@ foo bar for dst, context in articles: assert isinstance(dst, str) assert isinstance(context, dict) - assert 'content' in context + assert "content" in context assert isinstance(pages, list) assert len(pages) == 1 for dst, context in pages: assert isinstance(dst, str) assert isinstance(context, dict) - assert 'content' in context + assert "content" in context def test_build(args: Namespace) -> None: @@ -268,60 +268,63 @@ 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: + 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') + with open(f"{args.static_dir}/test", "w") as fh: + fh.write("hello") - os.mkdir(f'{args.input_dir}/testdir') - with open(f'{args.input_dir}/testdir/test', 'w') as fh: - fh.write('hello') + os.mkdir(f"{args.input_dir}/testdir") + with open(f"{args.input_dir}/testdir/test", "w") as fh: + fh.write("hello") blag.build(args) # test existence of the three converted files for i in range(3): - assert os.path.exists(f'{args.output_dir}/{i}.html') + assert os.path.exists(f"{args.output_dir}/{i}.html") # ... static file - assert os.path.exists(f'{args.output_dir}/test') + assert os.path.exists(f"{args.output_dir}/test") # ... directory - assert os.path.exists(f'{args.output_dir}/testdir/test') + assert os.path.exists(f"{args.output_dir}/testdir/test") # ... feed - assert os.path.exists(f'{args.output_dir}/atom.xml') + assert os.path.exists(f"{args.output_dir}/atom.xml") + # ... index + assert os.path.exists(f"{args.output_dir}/index.html") # ... archive - assert os.path.exists(f'{args.output_dir}/index.html') + assert os.path.exists(f"{args.output_dir}/archive.html") # ... tags - assert os.path.exists(f'{args.output_dir}/tags/index.html') - assert os.path.exists(f'{args.output_dir}/tags/foo.html') - assert os.path.exists(f'{args.output_dir}/tags/bar.html') + 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") @pytest.mark.parametrize( - 'template', + "template", [ - 'page.html', - 'article.html', - 'archive.html', - 'tags.html', - 'tag.html', - ] + "page.html", + "article.html", + "index.html", + "archive.html", + "tags.html", + "tag.html", + ], ) def test_missing_template_raises(template: str, args: Namespace) -> None: - os.remove(f'templates/{template}') + os.remove(f"templates/{template}") with pytest.raises(SystemExit): blag.build(args) def test_main(cleandir: str) -> None: - blag.main(['build']) + blag.main(["build"]) def test_cli_version(capsys: CaptureFixture[str]) -> None: with pytest.raises(SystemExit) as ex: - blag.main(['--version']) + blag.main(["--version"]) # normal system exit assert ex.value.code == 0 # proper version reported @@ -330,8 +333,8 @@ def test_cli_version(capsys: CaptureFixture[str]) -> None: def test_cli_verbose(cleandir: str, caplog: LogCaptureFixture) -> None: - blag.main(['build']) - assert 'DEBUG' not in caplog.text + blag.main(["build"]) + assert "DEBUG" not in caplog.text - blag.main(['--verbose', 'build']) - assert 'DEBUG' in caplog.text + blag.main(["--verbose", "build"]) + assert "DEBUG" in caplog.text diff --git a/tests/test_devserver.py b/tests/test_devserver.py index 4b66b38..29477fb 100644 --- a/tests/test_devserver.py +++ b/tests/test_devserver.py @@ -1,7 +1,8 @@ # remove when we don't support py38 anymore from __future__ import annotations -import time + import threading +import time from argparse import Namespace import pytest @@ -11,17 +12,17 @@ from blag import devserver def test_get_last_modified(cleandir: str) -> None: # take initial time - t1 = devserver.get_last_modified(['content']) + t1 = devserver.get_last_modified(["content"]) # wait a bit, create a file and measure again time.sleep(0.1) - with open('content/test', 'w') as fh: - fh.write('boo') - t2 = devserver.get_last_modified(['content']) + with open("content/test", "w") as fh: + fh.write("boo") + t2 = devserver.get_last_modified(["content"]) # wait a bit and take time again time.sleep(0.1) - t3 = devserver.get_last_modified(['content']) + t3 = devserver.get_last_modified(["content"]) assert t2 > t1 assert t2 == t3 @@ -29,20 +30,20 @@ def test_get_last_modified(cleandir: str) -> None: 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') + 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']) + t0 = devserver.get_last_modified(["build"]) t.start() # try for 5 seconds... for i in range(5): time.sleep(1) - t1 = devserver.get_last_modified(['build']) + t1 = devserver.get_last_modified(["build"]) print(t1) if t1 > t0: break @@ -60,16 +61,16 @@ def test_autoreload(args: Namespace) -> None: ) t.start() - t0 = devserver.get_last_modified(['build']) + t0 = devserver.get_last_modified(["build"]) # create a dummy file that can be build - with open('content/test.md', 'w') as fh: - fh.write('boo') + with open("content/test.md", "w") as fh: + fh.write("boo") # try for 5 seconds to see if we rebuild once... for i in range(5): time.sleep(1) - t1 = devserver.get_last_modified(['build']) + t1 = devserver.get_last_modified(["build"]) if t1 > t0: break assert t1 > t0 diff --git a/tests/test_markdown.py b/tests/test_markdown.py index 816f310..3035608 100644 --- a/tests/test_markdown.py +++ b/tests/test_markdown.py @@ -1,10 +1,11 @@ # remove when we don't support py38 anymore from __future__ import annotations + from datetime import datetime from typing import Any -import pytest import markdown +import pytest from blag.markdown import convert_markdown, markdown_factory @@ -13,23 +14,23 @@ from blag.markdown import convert_markdown, markdown_factory "input_, expected", [ # inline - ('[test](test.md)', 'test.html'), - ('[test](test.md "test")', 'test.html'), - ('[test](a/test.md)', 'a/test.html'), - ('[test](a/test.md "test")', 'a/test.html'), - ('[test](/test.md)', '/test.html'), - ('[test](/test.md "test")', '/test.html'), - ('[test](/a/test.md)', '/a/test.html'), - ('[test](/a/test.md "test")', '/a/test.html'), + ("[test](test.md)", "test.html"), + ('[test](test.md "test")', "test.html"), + ("[test](a/test.md)", "a/test.html"), + ('[test](a/test.md "test")', "a/test.html"), + ("[test](/test.md)", "/test.html"), + ('[test](/test.md "test")', "/test.html"), + ("[test](/a/test.md)", "/a/test.html"), + ('[test](/a/test.md "test")', "/a/test.html"), # reference - ('[test][]\n[test]: test.md ' '', 'test.html'), - ('[test][]\n[test]: test.md "test"', 'test.html'), - ('[test][]\n[test]: a/test.md', 'a/test.html'), - ('[test][]\n[test]: a/test.md "test"', 'a/test.html'), - ('[test][]\n[test]: /test.md', '/test.html'), - ('[test][]\n[test]: /test.md "test"', '/test.html'), - ('[test][]\n[test]: /a/test.md', '/a/test.html'), - ('[test][]\n[test]: /a/test.md "test"', '/a/test.html'), + ("[test][]\n[test]: test.md " "", "test.html"), + ('[test][]\n[test]: test.md "test"', "test.html"), + ("[test][]\n[test]: a/test.md", "a/test.html"), + ('[test][]\n[test]: a/test.md "test"', "a/test.html"), + ("[test][]\n[test]: /test.md", "/test.html"), + ('[test][]\n[test]: /test.md "test"', "/test.html"), + ("[test][]\n[test]: /a/test.md", "/a/test.html"), + ('[test][]\n[test]: /a/test.md "test"', "/a/test.html"), ], ) def test_convert_markdown_links(input_: str, expected: str) -> None: @@ -42,11 +43,11 @@ def test_convert_markdown_links(input_: str, expected: str) -> None: "input_, expected", [ # scheme - ('[test](https://)', 'https://'), + ("[test](https://)", "https://"), # netloc - ('[test](//test.md)', '//test.md'), + ("[test](//test.md)", "//test.md"), # no path - ('[test]()', ''), + ("[test]()", ""), ], ) def test_dont_convert_normal_links(input_: str, expected: str) -> None: @@ -58,13 +59,13 @@ def test_dont_convert_normal_links(input_: str, expected: str) -> None: @pytest.mark.parametrize( "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']}), + ("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()}, + "date: 2020-01-01 12:10", + {"date": datetime(2020, 1, 1, 12, 10).astimezone()}, ), ], ) @@ -88,9 +89,9 @@ this --- is -- a test ... """ html, meta = convert_markdown(md, md1) - assert 'mdash' in html - assert 'ndash' in html - assert 'hellip' in html + assert "mdash" in html + assert "ndash" in html + assert "hellip" in html def test_smarty_code() -> None: @@ -102,6 +103,6 @@ this --- is -- a test ... ``` """ html, meta = convert_markdown(md, md1) - assert 'mdash' not in html - assert 'ndash' not in html - assert 'hellip' not in html + assert "mdash" not in html + assert "ndash" not in html + assert "hellip" not in html diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py index 95b54c1..c976a29 100644 --- a/tests/test_quickstart.py +++ b/tests/test_quickstart.py @@ -1,5 +1,6 @@ # remove when we don't support py38 anymore from __future__ import annotations + import os from pytest import MonkeyPatch @@ -8,33 +9,37 @@ from blag.quickstart import get_input, quickstart def test_get_input_default_answer(monkeypatch: MonkeyPatch) -> None: - monkeypatch.setattr('builtins.input', lambda x: '') + monkeypatch.setattr("builtins.input", lambda x: "") answer = get_input("foo", "bar") - assert answer == 'bar' + assert answer == "bar" def test_get_input(monkeypatch: MonkeyPatch) -> None: - monkeypatch.setattr('builtins.input', lambda x: 'baz') + monkeypatch.setattr("builtins.input", lambda x: "baz") answer = get_input("foo", "bar") - assert answer == 'baz' + assert answer == "baz" def test_quickstart(cleandir: str, monkeypatch: MonkeyPatch) -> None: - monkeypatch.setattr('builtins.input', lambda x: 'foo') + monkeypatch.setattr("builtins.input", lambda x: "foo") quickstart(None) - with open('config.ini', 'r') as fh: + 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 + assert "base_url = foo" in data + assert "title = foo" in data + assert "description = foo" in data + assert "author = foo" in data for template in ( "archive.html", "article.html", "base.html", + "index.html", "page.html", "tag.html", "tags.html", ): - assert os.path.exists(f'templates/{template}') + assert os.path.exists(f"templates/{template}") + + for directory in "build", "content", "static": + assert os.path.exists(directory) diff --git a/tests/test_templates.py b/tests/test_templates.py index b25388e..8fd0265 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -1,5 +1,6 @@ # remove when we don't support py38 anymore from __future__ import annotations + import datetime from jinja2 import Template @@ -7,71 +8,91 @@ from jinja2 import Template def test_page(page_template: Template) -> None: ctx = { - 'content': 'this is the content', - 'title': 'this is the title', + "content": "this is the content", + "title": "this is the title", } result = page_template.render(ctx) - assert 'this is the content' in result - assert 'this is the title' in result + assert "this is the content" in result + assert "this is the title" in result def test_article(article_template: Template) -> None: ctx = { - 'content': 'this is the content', - 'title': 'this is the title', - 'date': datetime.datetime(1980, 5, 9), + "content": "this is the content", + "title": "this is the title", + "date": datetime.datetime(1980, 5, 9), } result = article_template.render(ctx) - assert 'this is the content' in result - assert 'this is the title' in result - assert '1980-05-09' in result + assert "this is the content" in result + assert "this is the title" in result + assert "1980-05-09" in result + + +def test_index(index_template: Template) -> None: + entry = { + "title": "this is a title", + "dst": "https://example.com/link", + "date": datetime.datetime(1980, 5, 9), + } + archive = [entry] + ctx = { + "archive": archive, + } + result = index_template.render(ctx) + assert "site title" in result + + assert "this is a title" in result + assert "1980-05-09" in result + assert "https://example.com/link" in result + + assert "/archive.html" in result def test_archive(archive_template: Template) -> None: entry = { - 'title': 'this is a title', - 'dst': 'https://example.com/link', - 'date': datetime.datetime(1980, 5, 9), + "title": "this is a title", + "dst": "https://example.com/link", + "date": datetime.datetime(1980, 5, 9), } archive = [entry] ctx = { - 'archive': archive, + "archive": archive, } result = archive_template.render(ctx) - assert 'site title' in result + assert "Archive" in result - assert 'this is a title' in result - assert '1980-05-09' in result - assert 'https://example.com/link' in result + assert "this is a title" in result + assert "1980-05-09" in result + assert "https://example.com/link" in result def test_tags(tags_template: Template) -> None: - tags = [('foo', 42)] + tags = [("foo", 42)] ctx = { - 'tags': tags, + "tags": tags, } result = tags_template.render(ctx) - assert 'Tags' in result + assert "Tags" in result - assert 'foo.html' in result - assert 'foo' in result - assert '42' in result + assert "foo.html" in result + assert "foo" in result + assert "42" in result def test_tag(tag_template: Template) -> None: entry = { - 'title': 'this is a title', - 'dst': 'https://example.com/link', - 'date': datetime.datetime(1980, 5, 9), + "title": "this is a title", + "dst": "https://example.com/link", + "date": datetime.datetime(1980, 5, 9), } archive = [entry] ctx = { - 'tag': 'foo', - 'archive': archive, + "tag": "foo", + "archive": archive, } result = tag_template.render(ctx) - assert 'Tag foo' in result + assert "foo" in result - assert 'this is a title' in result - assert '1980-05-09' in result - assert 'https://example.com/link' in result + assert "this is a title" in result + assert "1980-05-09" in result + assert "https://example.com/link" in result