From eb0c5072ced5ce4043c3fa8773da21b8db6bd74e Mon Sep 17 00:00:00 2001 From: Ben Hancock Date: Fri, 3 Oct 2025 19:51:17 -0700 Subject: [PATCH] Support loading plugins with config. Python-Markdown supports a number of extensions, and users of `blag` may wish to utilize those beyond the defaults. This set of changes supports loading those extensions by adding a line to the configuration file, while adding some checks to make sure that an error is raised if a non-existent extension is named. --- blag/blag.py | 30 +++++++++++++++++++++++++++--- blag/devserver.py | 2 +- blag/markdown.py | 32 ++++++++++++++++++++++++++------ docs/manual.md | 17 +++++++++++++++++ tests/conftest.py | 1 + tests/test_blag.py | 39 +++++++++++++++++++++++++++++++++++++++ tests/test_markdown.py | 16 +++++++++++++++- 7 files changed, 126 insertions(+), 11 deletions(-) diff --git a/blag/blag.py b/blag/blag.py index 99b70e2..ce1b45f 100644 --- a/blag/blag.py +++ b/blag/blag.py @@ -15,7 +15,12 @@ import blag from blag.devserver import serve -from blag.markdown import convert_markdown, markdown_factory +from blag.markdown import ( + MarkdownExtensionLoadError, + check_extensions, + convert_markdown, + markdown_factory, +) from blag.quickstart import quickstart from blag.version import __VERSION__ @@ -242,6 +247,12 @@ def build(args: argparse.Namespace) -> None: shutil.copytree(args.static_dir, args.output_dir, dirs_exist_ok=True) config = get_config("config.ini") + extra_extensions = get_extra_extensions(config) + try: + check_extensions(extra_extensions) + except MarkdownExtensionLoadError: + logger.error(f"Problem loading extensions: {extra_extensions}") + sys.exit(1) env = environment_factory(args.template_dir, dict(site=config)) @@ -268,6 +279,7 @@ def build(args: argparse.Namespace) -> None: args.output_dir, page_template, article_template, + extra_extensions, ) generate_feed( @@ -283,12 +295,21 @@ def build(args: argparse.Namespace) -> None: generate_tags(articles, tags_template, tag_template, args.output_dir) +def get_extra_extensions(config: configparser.SectionProxy) -> list[str]: + """Parse named extensions from config and return a list.""" + if extensions := config.get("extensions"): + return list(set(extensions.split(","))) + else: + return [] + + def process_markdown( convertibles: list[tuple[str, str]], input_dir: str, output_dir: str, page_template: Template, article_template: Template, + extra_extensions: list[str] = [], ) -> tuple[list[tuple[str, dict[str, Any]]], list[tuple[str, dict[str, Any]]]]: """Process markdown files. @@ -308,6 +329,9 @@ def process_markdown( output_dir page_template, archive_template templates for pages and articles + extra_extensions + additional supported markdown extensions. + see: https://python-markdown.github.io/extensions/ Returns ------- @@ -316,7 +340,7 @@ def process_markdown( """ logger.info("Converting Markdown files...") - md = markdown_factory() + md = markdown_factory(extra_extensions) articles = [] pages = [] @@ -396,7 +420,7 @@ def generate_feed( ) with open(f"{output_dir}/atom.xml", "w") as fh: - feed.write(fh, encoding='utf-8') + feed.write(fh, encoding="utf-8") def generate_index( diff --git a/blag/devserver.py b/blag/devserver.py index 2dd6481..73eb993 100644 --- a/blag/devserver.py +++ b/blag/devserver.py @@ -54,7 +54,7 @@ def get_last_modified(dirs: list[str]) -> float: return last_mtime -def autoreload(args: argparse.Namespace, wait: int=1) -> NoReturn: +def autoreload(args: argparse.Namespace, wait: int = 1) -> NoReturn: """Start the autoreloader. This method monitors the given directories for changes (i.e. the diff --git a/blag/markdown.py b/blag/markdown.py index 7b26375..a8ce240 100644 --- a/blag/markdown.py +++ b/blag/markdown.py @@ -17,7 +17,13 @@ logger = logging.getLogger(__name__) -def markdown_factory() -> Markdown: +class MarkdownExtensionLoadError(Exception): + """An error thrown when extensions cannot be loaded.""" + + pass + + +def markdown_factory(extra_extensions: list[str] = []) -> Markdown: """Create a Markdown instance. This method exists only to ensure we use the same Markdown instance @@ -28,14 +34,20 @@ def markdown_factory() -> Markdown: markdown.Markdown """ + default_extensions = [ + "codehilite", + "fenced_code", + "footnotes", + "meta", + "smarty", + ] + # Ensure the extra don't duplicate the default + all_extensions = list(set(default_extensions + extra_extensions)) + md = Markdown( extensions=[ - "meta", - "fenced_code", - "codehilite", - "smarty", - "footnotes", MarkdownLinkExtension(), + *all_extensions, ], output_format="html", ) @@ -132,3 +144,11 @@ def extendMarkdown(self, md: Markdown) -> None: "mdlink", 0, ) + + +def check_extensions(extensions: list[str]) -> None: + """Attempt to load the named extensions.""" + try: + Markdown(extensions=extensions) + except ModuleNotFoundError: + raise MarkdownExtensionLoadError diff --git a/docs/manual.md b/docs/manual.md index 072e508..c2f66cf 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -251,6 +251,23 @@ freely in you templates. However, some metadata elements are treated special: header and in the atom feed. +### Markdown Extensions + +`blag` loads several Markdown extensions by default, including `meta`, +`fenced_code`, `codehilite`, and `smarty`. If you wish to use additional +extensions, you may add a line to your configuration file as a comma-separated +list, like this: + +``` +extensions = footnotes,tables +``` + +You can view the full list of officially supported extensions for the +Python-Markdown libary here: + + + + ## Devserver blag provides a devserver which you can use for local web-development. The diff --git a/tests/conftest.py b/tests/conftest.py index 969a06b..6dfd58e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -92,5 +92,6 @@ def args(cleandir: Callable[[], Iterator[str]]) -> Iterator[Namespace]: output_dir="build", static_dir="static", template_dir="templates", + extensions=["footnotes"], ) yield args diff --git a/tests/test_blag.py b/tests/test_blag.py index 9311b39..b0bcf9e 100644 --- a/tests/test_blag.py +++ b/tests/test_blag.py @@ -192,6 +192,45 @@ def test_get_config() -> None: assert config_parsed["base_url"] == "https://example.com/" +def test_get_extra_extensions() -> None: + """Test parsing extensions string from config.""" + # No extensions in config + config = """ +[main] +base_url = https://example.com/ +title = title +description = description +author = a. u. thor + """ + with TemporaryDirectory() as dir: + configfile = f"{dir}/config.ini" + with open(configfile, "w") as fh: + fh.write(config) + + config_parsed = blag.get_config(configfile) + extra_extensions = blag.get_extra_extensions(config_parsed) + assert isinstance(extra_extensions, list) + assert len(extra_extensions) == 0 + + # Some extensions in the config + config = """ +[main] +base_url = https://example.com/ +title = title +description = description +author = a. u. thor +extensions = footnotes,tables + """ + with TemporaryDirectory() as dir: + configfile = f"{dir}/config.ini" + with open(configfile, "w") as fh: + fh.write(config) + config_parsed = blag.get_config(configfile) + extra_extensions = blag.get_extra_extensions(config_parsed) + assert "footnotes" in extra_extensions + assert "tables" in extra_extensions + + def test_environment_factory(cleandir: str) -> None: """Test environment_factory.""" globals_: dict[str, object] = {"foo": "bar", "test": "me"} diff --git a/tests/test_markdown.py b/tests/test_markdown.py index 3612319..eb1c0d2 100644 --- a/tests/test_markdown.py +++ b/tests/test_markdown.py @@ -6,7 +6,12 @@ import markdown import pytest -from blag.markdown import convert_markdown, markdown_factory +from blag.markdown import ( + MarkdownExtensionLoadError, + check_extensions, + convert_markdown, + markdown_factory, +) @pytest.mark.parametrize( @@ -125,3 +130,12 @@ def test_footnotes() -> None: assert "
" in html assert "
    " in html assert "footnotetext" in html + +def test_check_extensions() -> None: + """Test that bad extensions throw an error, good extensions don't.""" + ok_extensions = ["footnotes", "toc"] + check_extensions(ok_extensions) + + bad_extensions = ["foobar", "bazbom"] + with pytest.raises(MarkdownExtensionLoadError): + check_extensions(bad_extensions)