From dc2ac7be537ee1dce2ad6e07e47c287784feb6de Mon Sep 17 00:00:00 2001 From: John Franey <1728528+johnfraney@users.noreply.github.com> Date: Sun, 28 Apr 2024 11:20:04 -0300 Subject: [PATCH] feat: add blurry_image Jinja extension (#75) Adds blurry_image extension to insert an tag with width & height into a Jinja template. Optionally inserts an image of a specific size. Usage: ``` {% blurry_image page.image, width=250 %} ``` --- blurry/__init__.py | 4 + blurry/cli.py | 6 +- blurry/plugins/__init__.py | 1 + blurry/plugins/jinja_plugins/__init__.py | 0 .../jinja_plugins/blurry_image_extension.py | 75 +++++++++++++++++++ .../plugins/write-a-jinja-extension-plugin.md | 30 ++++++++ docs/content/templates/syntax.md | 28 +++++++ docs/poetry.lock | 51 ++++++++++++- docs/templates/base.html | 1 + poetry.lock | 75 ++++++++++++++++++- pyproject.toml | 4 + 11 files changed, 270 insertions(+), 5 deletions(-) create mode 100644 blurry/plugins/jinja_plugins/__init__.py create mode 100644 blurry/plugins/jinja_plugins/blurry_image_extension.py create mode 100644 docs/content/plugins/write-a-jinja-extension-plugin.md diff --git a/blurry/__init__.py b/blurry/__init__.py index a3fedc6..7cbebe1 100644 --- a/blurry/__init__.py +++ b/blurry/__init__.py @@ -25,6 +25,7 @@ from blurry.markdown import convert_markdown_file_to_html from blurry.open_graph import open_graph_meta_tags from blurry.plugins import discovered_html_plugins +from blurry.plugins import discovered_jinja_extensions from blurry.plugins import discovered_jinja_filter_plugins from blurry.schema_validation import validate_front_matter_as_schema from blurry.settings import get_build_directory @@ -69,6 +70,9 @@ def get_jinja_env(): } ) ), + extensions=[ + jinja_extension.load() for jinja_extension in discovered_jinja_extensions + ], ) for filter_plugin in discovered_jinja_filter_plugins: try: diff --git a/blurry/cli.py b/blurry/cli.py index ef689b1..84e25c3 100644 --- a/blurry/cli.py +++ b/blurry/cli.py @@ -2,6 +2,7 @@ from rich.table import Table from blurry.plugins import discovered_html_plugins +from blurry.plugins import discovered_jinja_extensions from blurry.plugins import discovered_jinja_filter_plugins from blurry.plugins import discovered_markdown_plugins @@ -27,7 +28,10 @@ def print_plugin_table(): plugin_table.add_row( "\n".join([p.name for p in discovered_markdown_plugins]), "\n".join([p.name for p in discovered_html_plugins]), - "\n".join([p.name for p in discovered_jinja_filter_plugins]), + "\n".join( + [p.name for p in discovered_jinja_filter_plugins] + + [p.name for p in discovered_jinja_extensions] + ), ) console.print(plugin_table) diff --git a/blurry/plugins/__init__.py b/blurry/plugins/__init__.py index d17b6b1..cb26555 100644 --- a/blurry/plugins/__init__.py +++ b/blurry/plugins/__init__.py @@ -3,3 +3,4 @@ discovered_markdown_plugins = entry_points(group="blurry.markdown_plugins") discovered_html_plugins = entry_points(group="blurry.html_plugins") discovered_jinja_filter_plugins = entry_points(group="blurry.jinja_filter_plugins") +discovered_jinja_extensions = entry_points(group="blurry.jinja_extensions") diff --git a/blurry/plugins/jinja_plugins/__init__.py b/blurry/plugins/jinja_plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blurry/plugins/jinja_plugins/blurry_image_extension.py b/blurry/plugins/jinja_plugins/blurry_image_extension.py new file mode 100644 index 0000000..041716f --- /dev/null +++ b/blurry/plugins/jinja_plugins/blurry_image_extension.py @@ -0,0 +1,75 @@ +import mimetypes +from pathlib import Path +from urllib.parse import urlparse + +from jinja2_simple_tags import StandaloneTag +from rich.console import Console +from wand.exceptions import BlobError +from wand.image import Image + +from blurry.images import add_image_width_to_path +from blurry.settings import get_build_directory +from blurry.utils import build_path_to_url + +warning_console = Console(stderr=True, style="bold yellow") + + +class BlurryImage(StandaloneTag): + safe_output = True + tags = {"blurry_image"} + + def render(self, *args, **kwargs): + (image_url, width) = args + image_content_path: str = "." + urlparse(image_url).path + image_path = get_build_directory() / image_content_path + + try: + with Image(filename=str(image_path)) as image: + image_width = image.width + image_height = image.height + image_mimetype = image.mimetype + except BlobError: + warning_console.print(f"Could not find image: {image_path}") + return "" + + attributes = { + "width": image_width, + "height": image_height, + } + for attribute_key in kwargs: + if attribute_key in ["width", "height"]: + warning_console.print( + f"blurry_image: Received {attribute_key} in template {self.template} but this attribute is dynamic. Skipping." + ) + continue + attributes[attribute_key] = kwargs.get(attribute_key) + + if width: + image_path = add_image_width_to_path(image_path, width) + + if image_mimetype in [ + mimetypes.types_map[".jpg"], + mimetypes.types_map[".png"], + ]: + image_path = Path(str(image_path).replace(image_path.suffix, ".avif")) + + if not image_path.exists(): + warning_console.print( + f"blurry_image: Could not find {image_path}. Skipping." + ) + return "" + + attributes["src"] = build_path_to_url(image_path) + + if "alt" not in attributes: + warning_console.print( + f"blurry_image: alt attribute missing for image in {self.template}. " + "This can negatively affect accessibility. " + "Use an empty alt tag if an image is only for show." + ) + + attributes_str = " ".join( + f'{name}="{value}"' for name, value in attributes.items() + ) + + return f"" diff --git a/docs/content/plugins/write-a-jinja-extension-plugin.md b/docs/content/plugins/write-a-jinja-extension-plugin.md new file mode 100644 index 0000000..64ff30d --- /dev/null +++ b/docs/content/plugins/write-a-jinja-extension-plugin.md @@ -0,0 +1,30 @@ ++++ +"@type" = "WebPage" +name = "Plugins: write a Jinja extension plugin" +abstract = "Documentation for Blurry's Jinja extension plugins" +datePublished = 2024-04-28 ++++ + +# Plugins: write an Jinja extension plugin + +Blurry makes it easy to add [custom Jinja extensions](https://jinja.palletsprojects.com/en/3.1.x/extensions/) to your site. +What is a Jinja extension? +From the Jinja docs: + +> Jinja supports extensions that can add extra filters, tests, globals or even extend the parser. The main motivation of extensions is to move often used code into a reusable class like adding support for internationalization. + +With [custom extensions](https://jinja.palletsprojects.com/en/3.1.x/extensions/#module-jinja2.ext) you can add custom tags to Jinja, like Blurry's `{% blurry_image %}` tag. + +## Example: `{% blurry_image %}` + +This tag finds the optimized version of an image at the specified URL, and optionally of the specified size. +You can find it in Blurry's source code in `blurry/plugins/jinja_plugins/blurry_image_extension.py`. + +Under the hood the extension uses [`jinja2-simple-tags`](https://github.com/dldevinc/jinja2-simple-tags) to simplify the process of writing a custom extension. + +To use a custom Jinja extension you've developed, add the appropriate plugin syntax to your project's `pyproject.toml` file: + +```toml +[tool.poetry.plugins."blurry.jinja_extensions"] +stars = "{{ yourproject.your_extension_file }}:YourExtension" +``` diff --git a/docs/content/templates/syntax.md b/docs/content/templates/syntax.md index 150b0fe..9b07fb2 100644 --- a/docs/content/templates/syntax.md +++ b/docs/content/templates/syntax.md @@ -3,6 +3,7 @@ name = "Templates: syntax" abstract = "Documentation for Blurry's template files and Jinja syntax" datePublished = 2023-04-09 +dateModified = 2023-04-28 image = {contentUrl = "../images/schema.org-logo.png"} +++ @@ -42,3 +43,30 @@ If your templates require more granularity than the Schema.org types, you can wr [blurry.template_schema_types] ContextWebPage = 'WebPage' ``` + +## Blurry-included plugins + +Blurry ships with some plugins to simplify writing templates. + +### `{% blurry_image %}` + +This extension adds the `{% blurry_image %}` tag to simplify including images reference in [Markdown front matter](../content/markdown.md) in your templates. +It does a few things: + +- Finds the image in your build directory +- Extracts the images width & height +- Builds an `` tag with width, height, and the othwer attributes specified in the tag + +#### Examples + +Basic example: + +```jinja +{% blurry_image page.thumbnailUrl, alt="Image description" %} +``` + +Example with explicit width (image with this width must be present in the build folder): + +```jinja +{% blurry_image page.thumbnailUrl, 250, id="image-id", class="responsive-image", loading="lazy" %} +``` diff --git a/docs/poetry.lock b/docs/poetry.lock index 2d64f50..022e451 100644 --- a/docs/poetry.lock +++ b/docs/poetry.lock @@ -1,9 +1,10 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. [[package]] name = "annotated-types" version = "0.6.0" description = "Reusable constraint types to use with typing.Annotated" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -13,8 +14,9 @@ files = [ [[package]] name = "blurry-cli" -version = "0.7.0" +version = "0.7.2" description = "A Mistune-based static site generator for Python" +category = "main" optional = false python-versions = "^3.10" files = [] @@ -25,6 +27,7 @@ dpath = "^2.1.6" ffmpeg-python = "^0.2.0" htmlmin2 = "^0.1.13" Jinja2 = "^3.0.0" +jinja2-simple-tags = "^0.6.1" livereload = "^2.6.3" mistune = "^3.0.0rc5" pydantic2-schemaorg = "^0.1.1" @@ -43,6 +46,7 @@ url = ".." name = "blurry-plugin-blur-blurry-name" version = "0.1.0" description = "A simple plugin to blur 'Blurry' in the Blurry documentation" +category = "main" optional = false python-versions = "^3.10" files = [] @@ -62,6 +66,7 @@ resolved_reference = "4255c21c98fe9c24eec00257d613ff7aecc23b66" name = "cachetools" version = "5.3.3" description = "Extensible memoizing collections and decorators" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -73,6 +78,7 @@ files = [ name = "click" version = "8.1.7" description = "Composable command line interface toolkit" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -87,6 +93,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -98,6 +105,7 @@ files = [ name = "dpath" version = "2.1.6" description = "Filesystem-like pathing and searching for dictionaries" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -109,6 +117,7 @@ files = [ name = "ffmpeg-python" version = "0.2.0" description = "Python bindings for FFmpeg - with complex filtering support" +category = "main" optional = false python-versions = "*" files = [ @@ -126,6 +135,7 @@ dev = ["Sphinx (==2.1.0)", "future (==0.17.1)", "numpy (==1.16.4)", "pytest (==4 name = "frozendict" version = "2.4.1" description = "A simple immutable dictionary" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -171,6 +181,7 @@ files = [ name = "future" version = "1.0.0" description = "Clean single-source support for Python 3 and 2" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -182,6 +193,7 @@ files = [ name = "htmlmin2" version = "0.1.13" description = "An HTML Minifier" +category = "main" optional = false python-versions = "*" files = [ @@ -192,6 +204,7 @@ files = [ name = "jinja2" version = "3.1.3" description = "A very fast and expressive template engine." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -205,10 +218,26 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "jinja2-simple-tags" +version = "0.6.1" +description = "Base classes for quick-and-easy template tag development" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "jinja2-simple-tags-0.6.1.tar.gz", hash = "sha256:54abf83883dcd13f8fd2ea2c42feeea8418df3640907bd5251dec5e25a6af0e3"}, + {file = "jinja2_simple_tags-0.6.1-py2.py3-none-any.whl", hash = "sha256:7b7cfa92f6813a1e0f0b61b9efcab60e6793674753e1f784ff270542e80ae20f"}, +] + +[package.dependencies] +Jinja2 = ">=2.10" + [[package]] name = "livereload" version = "2.6.3" description = "Python LiveReload is an awesome tool for web developers" +category = "main" optional = false python-versions = "*" files = [ @@ -224,6 +253,7 @@ tornado = {version = "*", markers = "python_version > \"2.7\""} name = "lxml" version = "5.2.1" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -395,6 +425,7 @@ source = ["Cython (>=3.0.10)"] name = "markdown-it-py" version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -419,6 +450,7 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "markupsafe" version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -488,6 +520,7 @@ files = [ name = "mdurl" version = "0.1.2" description = "Markdown URL utilities" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -499,6 +532,7 @@ files = [ name = "mistune" version = "3.0.2" description = "A sane and fast Markdown parser with useful plugins and renderers" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -510,6 +544,7 @@ files = [ name = "pydantic" version = "2.6.4" description = "Data validation using Python type hints" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -529,6 +564,7 @@ email = ["email-validator (>=2.0.0)"] name = "pydantic-core" version = "2.16.3" description = "" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -620,6 +656,7 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" name = "pydantic2-schemaorg" version = "0.1.1" description = "Pydantic classes for Schema.org" +category = "main" optional = false python-versions = "<4.0,>=3.10" files = [ @@ -634,6 +671,7 @@ pydantic = ">=2.6.1,<3.0.0" name = "pygments" version = "2.17.2" description = "Pygments is a syntax highlighting package written in Python." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -649,6 +687,7 @@ windows-terminal = ["colorama (>=0.4.6)"] name = "pyld" version = "2.0.4" description = "Python implementation of the JSON-LD API" +category = "main" optional = false python-versions = "*" files = [ @@ -671,6 +710,7 @@ requests = ["requests"] name = "rich" version = "13.7.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -689,6 +729,7 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] name = "selectolax" version = "0.3.21" description = "Fast HTML5 parser with CSS selectors." +category = "main" optional = false python-versions = "*" files = [ @@ -757,6 +798,7 @@ cython = ["Cython (==0.29.36)"] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -768,6 +810,7 @@ files = [ name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -779,6 +822,7 @@ files = [ name = "tornado" version = "6.4" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +category = "main" optional = false python-versions = ">= 3.8" files = [ @@ -799,6 +843,7 @@ files = [ name = "typer" version = "0.6.1" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -819,6 +864,7 @@ test = ["black (>=22.3.0,<23.0.0)", "coverage (>=5.2,<6.0)", "isort (>=5.0.6,<6. name = "typing-extensions" version = "4.11.0" description = "Backported and Experimental Type Hints for Python 3.8+" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -830,6 +876,7 @@ files = [ name = "wand" version = "0.6.13" description = "Ctypes-based simple MagickWand API binding for Python" +category = "main" optional = false python-versions = "*" files = [ diff --git a/docs/templates/base.html b/docs/templates/base.html index fb7cf72..22502da 100644 --- a/docs/templates/base.html +++ b/docs/templates/base.html @@ -63,6 +63,7 @@