mirror of
https://git.ari.lt/ari.lt/blog.ari.lt.git
synced 2025-02-04 09:39:25 +01:00
update @ Sun Dec 15 02:53:32 EET 2024
Signed-off-by: Ari Archer <ari@ari.lt>
This commit is contained in:
parent
b880ee9a5b
commit
bcf213414f
3 changed files with 265 additions and 3 deletions
|
@ -3,6 +3,7 @@
|
||||||
--qb: #ede9d3;
|
--qb: #ede9d3;
|
||||||
--qf: #e6e0c8;
|
--qf: #e6e0c8;
|
||||||
--lf: #d3ceba;
|
--lf: #d3ceba;
|
||||||
|
--ef: #bb6868;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
|
@ -42,3 +43,24 @@ blockquote:before {
|
||||||
#legal {
|
#legal {
|
||||||
color: var(--lf);
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -1,2 +1,4 @@
|
||||||
pyfzf
|
pyfzf
|
||||||
requests
|
requests
|
||||||
|
pillow
|
||||||
|
python-magic
|
||||||
|
|
244
scripts/blog.py
244
scripts/blog.py
|
@ -28,6 +28,7 @@ import mistune.inline_parser
|
||||||
import mistune.plugins
|
import mistune.plugins
|
||||||
import unidecode
|
import unidecode
|
||||||
import web_mini
|
import web_mini
|
||||||
|
from mistune.renderers.html import HTMLRenderer
|
||||||
from readtime import of_markdown as read_time_of_markdown # type: ignore
|
from readtime import of_markdown as read_time_of_markdown # type: ignore
|
||||||
from readtime.result import Result as MarkdownResult # 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
|
__version__: typing.Final[int] = 2
|
||||||
GEN: typing.Final[str] = f"ari-web blog generator version {__version__}"
|
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
|
OK: typing.Final[int] = 0
|
||||||
ER: typing.Final[int] = 1
|
ER: typing.Final[int] = 1
|
||||||
|
@ -415,10 +429,14 @@ STATS_TEMPLATE: typing.Final[str] = (
|
||||||
if NCI:
|
if NCI:
|
||||||
import http.server
|
import http.server
|
||||||
|
|
||||||
|
import magic
|
||||||
import pyfzf # type: ignore
|
import pyfzf # type: ignore
|
||||||
|
from PIL import Image
|
||||||
else:
|
else:
|
||||||
pyfzf: typing.Any = None
|
pyfzf: typing.Any = None
|
||||||
http: typing.Any = None
|
http: typing.Any = None
|
||||||
|
magic: typing.Any = None
|
||||||
|
Image: typing.Any = None
|
||||||
|
|
||||||
|
|
||||||
class Commands:
|
class Commands:
|
||||||
|
@ -656,6 +674,9 @@ def s_to_str(seconds: float) -> str:
|
||||||
# markdown
|
# markdown
|
||||||
|
|
||||||
TITLE_LINKS_RE: typing.Final[str] = r"<#:[^>]+?>"
|
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(
|
def parse_inline_titlelink(
|
||||||
|
@ -676,20 +697,114 @@ def parse_inline_titlelink(
|
||||||
return m.end()
|
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:
|
def titlelink(md: mistune.Markdown) -> None:
|
||||||
md.inline.register("titlelink", TITLE_LINKS_RE, parse_inline_titlelink, before="link") # type: ignore
|
md.inline.register("titlelink", TITLE_LINKS_RE, parse_inline_titlelink, before="link") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class BlogRenderer(mistune.HTMLRenderer):
|
def media_embed(md: mistune.Markdown) -> None:
|
||||||
def heading(self, text: str, level: int, **_: typing.Any) -> str:
|
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)
|
slug: str = slugify(text, [], 768, 768)
|
||||||
level = max(2, level)
|
level = max(2, level)
|
||||||
|
|
||||||
return f'<h{level} id="{slug}" h><a href="#{slug}">#</a> {text}</h{level}>'
|
return f'<h{level} id="{slug}" h><a href="#{slug}">#</a> {text}</h{level}>'
|
||||||
|
|
||||||
|
def strong_error(self, text: str) -> str:
|
||||||
|
return f"<strong class=error>{text}</strong>"
|
||||||
|
|
||||||
|
def block_div(self, text: str, classes: str):
|
||||||
|
return f'<div class="{html_escape(classes)}">{text}</div>'
|
||||||
|
|
||||||
|
def image(self, text: str, alt: str, type: str, width: int, height: int) -> str:
|
||||||
|
return f'<img title="{html_escape(alt)}" src="{text}" alt="{html_escape(alt)}" type="{type}" data-width="{width}" data-height="{height}" loading=lazy />'
|
||||||
|
|
||||||
|
def audio(self, text: str, alt: str, type: str) -> str:
|
||||||
|
return f'<audio controls title="{html_escape(alt)}"> <source src="{text}" type="{type}" {alt} /> </audio>'
|
||||||
|
|
||||||
|
|
||||||
def markdown(md: str, plugins: list[typing.Any]) -> str:
|
def markdown(md: str, plugins: list[typing.Any]) -> str:
|
||||||
return mistune.create_markdown(plugins=plugins + [titlelink], renderer=BlogRenderer())(md) # type: ignore
|
return mistune.create_markdown(plugins=plugins + [titlelink, media_embed], renderer=BlogRenderer())(md) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
# edit commands
|
# edit commands
|
||||||
|
@ -1499,6 +1614,123 @@ def blog(config: dict[str, typing.Any]) -> int:
|
||||||
return OK
|
return OK
|
||||||
|
|
||||||
|
|
||||||
|
@cmds.new
|
||||||
|
def media(config: dict[str, typing.Any]) -> int:
|
||||||
|
"""add media"""
|
||||||
|
|
||||||
|
path: str = iinput("media path")
|
||||||
|
path = os.path.expanduser(path)
|
||||||
|
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
return err(f"file {path!r} is not a file or does not exist")
|
||||||
|
|
||||||
|
purpose: str = iinput("media purpose")
|
||||||
|
title: str = iinput("media title")
|
||||||
|
license: str = iinput("media license (SPDX)")
|
||||||
|
credit: str = iinput("media credit")
|
||||||
|
|
||||||
|
# MIME stuff
|
||||||
|
|
||||||
|
mime: str = magic.from_file(path, mime=True)
|
||||||
|
|
||||||
|
if mime not in MEDIA_MIME:
|
||||||
|
return err(f"mime {mime!r} is not a supported media type")
|
||||||
|
|
||||||
|
ext: str = MEDIA_MIME[mime]
|
||||||
|
|
||||||
|
# Filename
|
||||||
|
|
||||||
|
sha256_hash: typing.Any = hashlib.sha256() # type: ignore
|
||||||
|
with open(path, "rb") as fp:
|
||||||
|
for byte_block in iter(lambda: fp.read(4096), b""):
|
||||||
|
sha256_hash.update(byte_block)
|
||||||
|
hash_hex: str = sha256_hash.hexdigest()
|
||||||
|
|
||||||
|
# Create media dir + media index
|
||||||
|
|
||||||
|
os.makedirs("media", exist_ok=True)
|
||||||
|
|
||||||
|
if os.path.exists("media/media.json"):
|
||||||
|
with open("media/media.json", "r") as fp:
|
||||||
|
index: dict[str, dict[str, typing.Any]] = json.load(fp)
|
||||||
|
else:
|
||||||
|
index = {}
|
||||||
|
|
||||||
|
# Check if it exists
|
||||||
|
|
||||||
|
if hash_hex in index:
|
||||||
|
return err(f"media pointing to {path!r} already exists")
|
||||||
|
|
||||||
|
# Process stuff
|
||||||
|
|
||||||
|
filename: str = f"{hash_hex}.{ext}"
|
||||||
|
fpath: str = f"media/{filename}"
|
||||||
|
|
||||||
|
if mime.startswith("image/"):
|
||||||
|
with Image.open(path) as img:
|
||||||
|
width, height = img.size
|
||||||
|
|
||||||
|
shutil.copy(path, fpath)
|
||||||
|
|
||||||
|
if ext in {"jpeg", "png"}:
|
||||||
|
quality_s: str = iinput("image quality % (1-100)")
|
||||||
|
|
||||||
|
try:
|
||||||
|
quality: int = int(quality_s)
|
||||||
|
quality = 100 if quality > 100 else quality
|
||||||
|
except Exception:
|
||||||
|
quality = 100
|
||||||
|
|
||||||
|
img.save(
|
||||||
|
fpath,
|
||||||
|
format=ext,
|
||||||
|
quality=quality,
|
||||||
|
optimize=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
index[hash_hex] = {
|
||||||
|
"type": "image",
|
||||||
|
"width": width,
|
||||||
|
"height": height,
|
||||||
|
"alt": iinput("alt text"),
|
||||||
|
}
|
||||||
|
elif mime.startswith("audio/"):
|
||||||
|
shutil.copy(path, fpath)
|
||||||
|
|
||||||
|
index[hash_hex] = {
|
||||||
|
"type": "audio",
|
||||||
|
"alt": iinput("alt text"),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return err(f"unsupported MIME: {mime!r}")
|
||||||
|
|
||||||
|
index[hash_hex].update(
|
||||||
|
{
|
||||||
|
"purpose": purpose,
|
||||||
|
"title": title,
|
||||||
|
"license": license,
|
||||||
|
"credit": credit,
|
||||||
|
"ext": ext,
|
||||||
|
"mime": mime,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
lnew(f"media {hash_hex} created")
|
||||||
|
|
||||||
|
# Update media.json
|
||||||
|
|
||||||
|
with open("media/media.json", "w") as fp:
|
||||||
|
json.dump(index, fp, indent=config["indent"])
|
||||||
|
|
||||||
|
with open("media/media_json_hash.txt", "w") as fp:
|
||||||
|
with open("media/media.json", "rb") as fk:
|
||||||
|
fp.write(hashlib.sha256(fk.read()).hexdigest())
|
||||||
|
|
||||||
|
lnew("Updated media.json and media_json_hash.txt")
|
||||||
|
|
||||||
|
return OK
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
"""entry / main function"""
|
"""entry / main function"""
|
||||||
|
|
||||||
|
@ -1518,6 +1750,12 @@ def main() -> int:
|
||||||
else:
|
else:
|
||||||
lnew("using the default config")
|
lnew("using the default config")
|
||||||
|
|
||||||
|
if os.path.exists("media/media.json"):
|
||||||
|
with open("media/media.json", "r") as fp:
|
||||||
|
MEDIA_INDEX.update(json.load(fp))
|
||||||
|
|
||||||
|
log("Loaded the media index (media/media.json)")
|
||||||
|
|
||||||
sort(cfg)
|
sort(cfg)
|
||||||
|
|
||||||
log(f"looking command {sys.argv[1]!r} up")
|
log(f"looking command {sys.argv[1]!r} up")
|
||||||
|
|
Loading…
Add table
Reference in a new issue