diff --git a/content/post_critical.css b/content/post_critical.css
index ce2f27a..ba1bdc3 100644
--- a/content/post_critical.css
+++ b/content/post_critical.css
@@ -3,6 +3,7 @@
--qb: #ede9d3;
--qf: #e6e0c8;
--lf: #d3ceba;
+ --ef: #bb6868;
}
pre {
@@ -42,3 +43,24 @@ blockquote:before {
#legal {
color: var(--lf);
}
+
+.error {
+ color: var(--ef);
+}
+
+.media {
+ text-align: center;
+ background-color: var(--cb);
+ padding: 1em;
+}
+
+.media > audio {
+ max-width: 600px;
+ width: 100%;
+}
+
+.media > img {
+ max-width: 400px;
+ width: 100%;
+ height: auto;
+}
diff --git a/requirements-extra.txt b/requirements-extra.txt
index 941c754..f4141ff 100644
--- a/requirements-extra.txt
+++ b/requirements-extra.txt
@@ -1,2 +1,4 @@
pyfzf
requests
+pillow
+python-magic
diff --git a/scripts/blog.py b/scripts/blog.py
index 71a4a7a..25139b8 100755
--- a/scripts/blog.py
+++ b/scripts/blog.py
@@ -28,6 +28,7 @@ import mistune.inline_parser
import mistune.plugins
import unidecode
import web_mini
+from mistune.renderers.html import HTMLRenderer
from readtime import of_markdown as read_time_of_markdown # type: ignore
from readtime.result import Result as MarkdownResult # type: ignore
@@ -37,6 +38,19 @@ from readtime.result import Result as MarkdownResult # type: ignore
__version__: typing.Final[int] = 2
GEN: typing.Final[str] = f"ari-web blog generator version {__version__}"
+MEDIA_MIME: dict[str, str] = {
+ "image/jpeg": "jpeg",
+ "image/png": "png",
+ "image/gif": "gif",
+ "image/webp": "webp",
+ "image/avif": "avif",
+ "image/svg+xml": "svg",
+ "audio/mpeg": "mp3",
+ "audio/wav": "wav",
+ "audio/ogg": "ogg",
+ "audio/flac": "flac",
+}
+
OK: typing.Final[int] = 0
ER: typing.Final[int] = 1
@@ -415,10 +429,14 @@ STATS_TEMPLATE: typing.Final[str] = (
if NCI:
import http.server
+ import magic
import pyfzf # type: ignore
+ from PIL import Image
else:
pyfzf: typing.Any = None
http: typing.Any = None
+ magic: typing.Any = None
+ Image: typing.Any = None
class Commands:
@@ -656,6 +674,9 @@ def s_to_str(seconds: float) -> str:
# markdown
TITLE_LINKS_RE: typing.Final[str] = r"<#:[^>]+?>"
+MEDIA_EMBED_RE: typing.Final[str] = r"<@:[^>]+?>"
+
+MEDIA_INDEX: dict[str, dict[str, typing.Any]] = {}
def parse_inline_titlelink(
@@ -676,20 +697,114 @@ def parse_inline_titlelink(
return m.end()
+def parse_inline_media_embed(
+ _: mistune.inline_parser.InlineParser,
+ m: re.Match[str],
+ state: mistune.core.InlineState,
+) -> int:
+ text: str = m.group(0)[3:-1]
+
+ if text not in MEDIA_INDEX:
+ state.append_token(
+ {
+ "type": "block_div",
+ "children": [
+ {
+ "type": "strong_error",
+ "raw": f"ERROR: Media '{html_escape(text)}' does not exist.",
+ }
+ ],
+ "attrs": {"classes": "media"},
+ }
+ )
+ return m.end()
+
+ mdx: dict[str, typing.Any] = MEDIA_INDEX[text]
+ source: str = f"/media/{text}.{mdx['ext']}"
+
+ if mdx["type"] == "image":
+ child: dict[str, typing.Any] = {
+ "type": "image",
+ "raw": source,
+ "attrs": {
+ "alt": mdx["alt"],
+ "type": mdx["mime"],
+ "width": mdx["width"],
+ "height": mdx["height"],
+ },
+ }
+ elif mdx["type"] == "audio":
+ child = {
+ "type": "audio",
+ "raw": source,
+ "attrs": {
+ "alt": mdx["alt"],
+ "type": mdx["mime"],
+ },
+ }
+ else:
+ state.append_token(
+ {
+ "type": "block_div",
+ "children": [
+ {
+ "type": "strong_error",
+ "raw": f"ERROR: Media '{html_escape(text)}' has an unsupported type.",
+ }
+ ],
+ "attrs": {"classes": "media"},
+ }
+ )
+ return m.end()
+
+ state.append_token(
+ {
+ "type": "block_div",
+ "children": [
+ child,
+ {"type": "linebreak"},
+ {
+ "type": "emphasis",
+ "raw": f"{mdx['alt']} | \"{mdx['title']}\" by {mdx['credit']} ({mdx['license']}). Purpose: {html_escape(mdx['purpose'])}.",
+ },
+ ],
+ "attrs": {"classes": "media"},
+ }
+ )
+
+ return m.end()
+
+
def titlelink(md: mistune.Markdown) -> None:
md.inline.register("titlelink", TITLE_LINKS_RE, parse_inline_titlelink, before="link") # type: ignore
-class BlogRenderer(mistune.HTMLRenderer):
- def heading(self, text: str, level: int, **_: typing.Any) -> str:
+def media_embed(md: mistune.Markdown) -> None:
+ md.inline.register("media_embed", MEDIA_EMBED_RE, parse_inline_media_embed, before="link") # type: ignore
+
+
+class BlogRenderer(HTMLRenderer):
+ def heading(self, text: str, level: int) -> str:
slug: str = slugify(text, [], 768, 768)
level = max(2, level)
return f'